
{
    "version": "https://jsonfeed.org/version/1",
    "title": "Maxime Vaillancourt",
    
    "home_page_url": "https://maximevaillancourt.com/",
    "feed_url": "https://maximevaillancourt.com/feed.json",
    "expired": false,
    "items": [

        {
            "id": "/blog/building-fully-static-guestbook-jekyll-netlify",
            "title": "Building a fully static guestbook using Jekyll and Netlify",
            "content_html": "<p><strong>In this blog post, we’re implementing a no-JavaScript guestbook system for statically-generated websites with built-in spam protection</strong>. That’s right: no JavaScript involved. Just good old HTTP and HTML working together… with a bit of Netlify magic. The solution I’m presenting here is specific to Netlify and Jekyll, but I’m pretty sure you could port this to other service/tool combinations.</p>\n\n<hr>\n\n<p>Sometime ago, I set out to build a guestbook for this website. I love the idea of a shared space where all visitors can share their experience and connect with others. An internet guestbook is like a hotel guestbook on steroids. Here’s an excerpt from my own guestbook (<a href=\"https://maximevaillancourt.com/guestbook\" class=\"internal-link\">view the live guestbook here</a>!):</p>\n\n<p><img src=\"/assets/guestbook.png\" alt=\"\"></p>\n\n<p>I didn’t, however, want to move from the static website setup I had to a full-blown WordPress setup or anything that had a server-side backend that would unnecessarily make things more complicated and potentially lead to a worse experience. Additionally, I did not want the guestbook to rely on JavaScript, because I respect those who turn off JavaScript when browsing the Internet. The guestbook would need to work for everyone regardless of their web browsing preferences.</p>\n\n<p>Lucky for us, we already solved this problem in the early 1990s: HTTP has a POST verb that a server can handle to receive data from the client. However, we run into another problem when considering that this is a static website (which is effectively just a bunch of HTML files linked together). There’s no dynamic back-end of any kind serving those HTML files to you: at the time of writing this, it’s all served straight from Netlify’s CDN.</p>\n\n<p>So what do we do?</p>\n\n<p>Well, this website was already off to a good start: it’s powered by Netlify, which offers an excellent <a href=\"https://www.netlify.com/products/forms/\" target=\"blank\">Forms</a> product, which provides an API. Once it’s set up, Netlify intercepts POST calls sent to a web page and runs logic in the background to store the contents of the form submission for later use via their API. In practice, this means that when someone goes on the guestbook and submits their comment using the form at the bottom of the page, Netlify intercepts the POST call and creates a database entry somewhere on <em>their</em> backend, which I can then query through their Forms API.</p>\n\n<p>Note that when someone submits a comment on the guestbook, it doesn’t automatically show up on the guestbook page: this is because I manually moderate comments by pulling them from the Netlify Forms API myself and then re-generate the website.</p>\n\n<p>The beauty in this workflow is two-fold:</p>\n\n<ol>\n  <li>Read requests (regular GETs) to the site are still just static HTML files, so they remain super fast and easily cacheable across a CDN;</li>\n  <li>There’s built-in comment moderation for me to weed out bad apples before the comments actually show up on the live guestbook.</li>\n</ol>\n\n<p>Make sense? Let’s see how to implement this. I’ll assume you already have a Netlify account and a site set up there. If not, do that, and come back when you’re done.</p>\n\n<p>Let’s start with the entrypoint: the actual HTML form itself.</p>\n\n<h2 id=\"1-create-an-html-form-on-your-website\">1. Create an HTML form on your website</h2>\n\n<p><a href=\"https://docs.netlify.com/forms/setup/\" target=\"blank\">Netlify’s excellent “Forms setup” documentation page</a> explains how to start using their Forms API: simply add an HTML form to your website, then publish it to the Internet. The first time someone fills out the form and submits it, you’ll get an email, and see your form created in the Forms section of your site in the Netlify admin panel. It’s that easy.</p>\n\n<p>Here’s the HTML form I added on my own guestbook page (<a href=\"https://raw.githubusercontent.com/maximevaillancourt/maximevaillancourt.com/e1efaf786ea9acceef802ab52176745a85ebc5e7/_pages/guestbook.md\" target=\"blank\">view source on GitHub</a>):</p>\n\n<div class=\"language-html highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nt\">&lt;div&gt;</span>\n  <span class=\"nt\">&lt;h2&gt;</span>Sign the guestbook<span class=\"nt\">&lt;/h2&gt;</span>\n\n  <span class=\"nt\">&lt;form</span> <span class=\"na\">action=</span><span class=\"s\">\"/submitted\"</span> <span class=\"na\">name=</span><span class=\"s\">\"guestbook\"</span> <span class=\"na\">netlify</span> <span class=\"na\">netlify-honeypot=</span><span class=\"s\">\"not-for-humans\"</span><span class=\"nt\">&gt;</span>\n    <span class=\"nt\">&lt;p</span> <span class=\"na\">style=</span><span class=\"s\">\"display: none;\"</span><span class=\"nt\">&gt;</span>\n      <span class=\"nt\">&lt;label&gt;</span>Don’t fill this out if you're human: <span class=\"nt\">&lt;input</span> <span class=\"na\">name=</span><span class=\"s\">\"not-for-humans\"</span> <span class=\"nt\">/&gt;&lt;/label&gt;</span>\n    <span class=\"nt\">&lt;/p&gt;</span>\n    <span class=\"nt\">&lt;p&gt;</span>\n      <span class=\"nt\">&lt;label&gt;</span>Name<span class=\"nt\">&lt;br&gt;&lt;input</span> <span class=\"na\">placeholder=</span><span class=\"s\">\"Your name...\"</span> <span class=\"na\">type=</span><span class=\"s\">\"text\"</span> <span class=\"na\">name=</span><span class=\"s\">\"name\"</span> <span class=\"nt\">/&gt;&lt;/label&gt;</span>\n    <span class=\"nt\">&lt;/p&gt;</span>\n    <span class=\"nt\">&lt;p&gt;</span>\n      <span class=\"nt\">&lt;label&gt;</span>Message<span class=\"nt\">&lt;br&gt;&lt;textarea</span> <span class=\"na\">name=</span><span class=\"s\">\"message\"</span> <span class=\"na\">placeholder=</span><span class=\"s\">\"Your message...\"</span><span class=\"nt\">&gt;&lt;/textarea&gt;&lt;/label&gt;</span>\n    <span class=\"nt\">&lt;/p&gt;</span>\n    <span class=\"nt\">&lt;p&gt;</span>\n      <span class=\"nt\">&lt;button</span> <span class=\"na\">type=</span><span class=\"s\">\"submit\"</span><span class=\"nt\">&gt;</span>Submit<span class=\"nt\">&lt;/button&gt;</span>\n    <span class=\"nt\">&lt;/p&gt;</span>\n  <span class=\"nt\">&lt;/form&gt;</span>\n<span class=\"nt\">&lt;/div&gt;</span>\n</code></pre></div></div>\n\n<p>Notice these few key points:</p>\n<ul>\n  <li>The <code class=\"language-plaintext highlighter-rouge\">netlify</code> attribute in the <code class=\"language-plaintext highlighter-rouge\">form</code> tag is what tells Netlify to actually care about this form. Without this, Netlify wouldn’t track submissions to this form.</li>\n  <li>There’s a honeypot field in the form (<code class=\"language-plaintext highlighter-rouge\">not-for-humans</code>). This field is not shown to humans browsing the page, so it should always be empty. However, bots tend to fill out all fields, so Netlify knows that if this field is not empty, it’s almost certainly a spam submission, so it knows it can simply ignore it.</li>\n  <li>The form <code class=\"language-plaintext highlighter-rouge\">name</code> attribute (in this case, <code class=\"language-plaintext highlighter-rouge\">guestbook</code>) is the identifier of the form, which you’ll see appear in the Netlify admin panel later when submissions start coming in.</li>\n</ul>\n\n<p>Once you have added this form to your site, publish it to the Internet, and submit the form on your live site to test it out. If all goes well, Netlify will give a success response. You can customize this success page by adding a Jekyll page at the <code class=\"language-plaintext highlighter-rouge\">/submitted</code> permalink URL, similar to <a href=\"https://github.com/maximevaillancourt/maximevaillancourt.com/blob/6cb4fc5f789b2a25d25fae1701b559a4880f4342/_pages/submitted.md\" target=\"blank\">what I’m doing on this website</a>.</p>\n\n<p>We now have a way to receive submissions from people browsing our website. Let’s now see how to pull these submissions from the Netlify Forms API and into our Jekyll website.</p>\n\n<h2 id=\"2-fetch-form-submissions-via-the-netlify-forms-api\">2. Fetch form submissions via the Netlify Forms API</h2>\n\n<p>I wrote a Ruby script (not very clean, PRs welcome!) that talks to the Netlify Forms API and pulls form submissions into a YAML file that Jekyll reads when generating the static site. <a href=\"https://github.com/maximevaillancourt/maximevaillancourt.com/blob/f8a7c2b5c9c21a78a04d3c65b36ae2b26aa26ad0/update_guestbook.rb\" target=\"blank\">You can download the script from my repository on GitHub</a>.</p>\n\n<p>You’ll need to install the HTTParty gem (<code class=\"language-plaintext highlighter-rouge\">gem install httparty</code>) and replace three values in the file (keep those values secret, don’t commit them to a public repository!):</p>\n<ul>\n  <li>\n<code class=\"language-plaintext highlighter-rouge\">NETLIFY_PERSONAL_ACCESS_TOKEN</code>: you can generate a personal access token in <a href=\"https://app.netlify.com/user/applications#personal-access-tokens\" target=\"blank\">your Netlify settings</a>.</li>\n  <li>\n<code class=\"language-plaintext highlighter-rouge\">NETLIFY_SITE_ID</code>: you can find this ID by going in your Netlify site settings (Site information ➞ API ID).</li>\n  <li>\n<code class=\"language-plaintext highlighter-rouge\">NETLIFY_FORM_ID</code>: you can find this ID by going in the Forms section of your Netlify site, clicking on the form in the list, and copying the last part of the URL (<code class=\"language-plaintext highlighter-rouge\">/sites/&lt;site_name&gt;/forms/&lt;FORM_ID&gt;</code>).</li>\n</ul>\n\n<p>Before running the script however, make sure to create a <code class=\"language-plaintext highlighter-rouge\">_data/</code> directory in your Jekyll site, and inside of it, an empty <code class=\"language-plaintext highlighter-rouge\">guestbook_messages.yml</code> file; this is where the script will write form submissions to for Jekyll to use when generating the site.</p>\n\n<p>Once you’re done, run the script with <code class=\"language-plaintext highlighter-rouge\">ruby update_guestbook.rb</code>. This should pull form submissions from the Netlify Forms API and write the entries to your site’s <code class=\"language-plaintext highlighter-rouge\">_data/guestbook_messages.yml</code> file. <a href=\"https://github.com/maximevaillancourt/maximevaillancourt.com/blob/2ef906a04abe744fbec575ea01fd3c565692fd1e/_data/guestbook_messages.yml\" target=\"blank\">Here’s how the file looks like in my own repository</a>.</p>\n\n<p>At this point, there’s only one step left: showing the form submissions in the <code class=\"language-plaintext highlighter-rouge\">_data/guestbook_messages.yml</code> on your site.</p>\n\n<h2 id=\"3-display-guestbook-submissions\">3. Display guestbook submissions</h2>\n\n<p>To show the entries from the YAML file on your site, add this snippet where you’d like to show the messages:</p>\n\n<div class=\"language-liquid highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"cp\">{%</span><span class=\"w\"> </span><span class=\"nt\">for</span><span class=\"w\"> </span><span class=\"nv\">message</span><span class=\"w\"> </span><span class=\"nt\">in</span><span class=\"w\"> </span><span class=\"nv\">site</span><span class=\"p\">.</span><span class=\"nv\">data</span><span class=\"p\">.</span><span class=\"nv\">guestbook_messages</span><span class=\"w\"> </span><span class=\"na\">reversed</span><span class=\"w\"> </span><span class=\"cp\">%}</span>\n  &lt;div&gt;\n    &lt;div&gt;&lt;span style=\"font-weight: bold;\"&gt;<span class=\"cp\">{{</span><span class=\"w\"> </span><span class=\"nv\">message</span><span class=\"p\">.</span><span class=\"nv\">author</span><span class=\"w\"> </span><span class=\"cp\">}}</span>&lt;/span&gt; (<span class=\"cp\">{{</span><span class=\"w\"> </span><span class=\"nv\">message</span><span class=\"p\">.</span><span class=\"nv\">timestamp</span><span class=\"w\"> </span><span class=\"p\">|</span><span class=\"w\"> </span><span class=\"nf\">date</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"%Y-%m-%d\"</span><span class=\"w\"> </span><span class=\"cp\">}}</span>)&lt;/div&gt;\n    &lt;div&gt;<span class=\"cp\">{{</span><span class=\"w\"> </span><span class=\"nv\">message</span><span class=\"p\">.</span><span class=\"nv\">body</span><span class=\"w\"> </span><span class=\"cp\">}}</span>&lt;/div&gt;\n  &lt;/div&gt;\n<span class=\"cp\">{%</span><span class=\"w\"> </span><span class=\"nt\">endfor</span><span class=\"w\"> </span><span class=\"cp\">%}</span>\n</code></pre></div></div>\n\n<p>At this point, your site should show the entries in the YAML file that the Ruby script wrote to after fetching the form submissions from the Netlify Forms API.</p>\n\n<p>All you need to do now is to generate your static site and upload it somewhere on the Internet.</p>\n\n<p>The next time someone sends a form submission on your site, you’ll get an email from Netlify, after which you can run the Ruby script to moderate the comment, and if it’s acceptable, commit the changes, then push them to Netlify to update your site. In the event that you don’t want to show a specific form submission on your site, log in to your Netlify admin panel, go to your site’s Forms section, open the form, then delete the entry.</p>\n\n<p>A potential next step could be to extend this to every single blog post to effectively create a commenting system (rather than a single guestbook page)… but that’s for another time.</p>\n",
            "url": "https://maximevaillancourt.com/blog/building-fully-static-guestbook-jekyll-netlify",
            "summary": "No JavaScript. Just good old HTTP and HTML working together.",
            "date_published": "2021-08-08 00:00:00 +0000"
            
        },

        {
            "id": "/blog/moving-away-from-google-services-8-years-in",
            "title": "Moving away from Google services, 8 years in",
            "content_html": "<p>On July 1st, 2013, Google shut down Google Reader forever. On that day, I understood that my personal data was at the mercy of a company that might just shut down access to it whenever business incentives lined up.</p>\n\n<p>My digital life almost entirely used to be stored within Google services: emails, personal files, to do lists, photos, RSS subscriptions, contacts, passwords, music playlists, apps, etc. Since then, I’ve been on a journey to reduce my reliance on Google products and to put my eggs in more privacy-friendly baskets, and I’ve come to realize that having agency over my own data is really, really satisfying.</p>\n\n<p><strong>This blog post covers how I migrated away from a dozen Google services to privacy-friendly (and sometimes self-hosted) services instead</strong>. Let’s go over those one-by-one and see how they all work (and how you can set them up yourself) Hint: it’s quite fun.</p>\n\n<p>Before we start, it’s worth pointing out my personal device setup:</p>\n<ul>\n  <li>Server: <a href=\"https://www.canakit.com/raspberry-pi-3-model-b-plus-starter-kit.html\" target=\"blank\">Raspberry Pi 3B+</a> (with a static IP on my LAN)</li>\n  <li>Android-based smartphone</li>\n  <li>Linux-based laptop (that’s right, <a href=\"https://maximevaillancourt.com/blog/why-i-use-a-thinkpad-x220-in-2021\" class=\"internal-link\">my trusty ThinkPad X220</a>)</li>\n</ul>\n\n<p>Okay, with that out of the way, let’s jump in!</p>\n\n<h2 id=\"google-reader--miniflux-self-hosted\">Google Reader ➞ Miniflux (self-hosted)</h2>\n\n<p>Here it is: the original trigger. When Google Reader shut down, I moved my subscriptions to <a href=\"https://feedly.com/\" target=\"blank\">Feedly</a>, then to <a href=\"https://feedbin.com/\" target=\"blank\">Feedbin</a>, and eventually I decided to self-host <a href=\"https://miniflux.app/\" target=\"blank\">Miniflux</a> on the Raspberry Pi. Miniflux is super easy to install: it’s a single binary, and from that I simply created a service that runs the binary whenever the Raspberry Pi boots up.</p>\n\n<p>The nice thing about Miniflux is that it provides a Web UI, which I use when reading feeds from my laptop. I simply punch in the IP address of my Raspberry Pi, tack the port where the Miniflux service is running at, and log in. Easy!</p>\n\n<p>For Android however, I use a neat little app called <a href=\"https://play.google.com/store/apps/details?id=com.constantin.constaflux\" target=\"blank\">Constaflux</a>. Setting it up is easy too: punch in the IP address and port of your Miniflux service, username and password, and connect. From there, you have a native application that lets you read your RSS feeds on your Android device.</p>\n\n<p>Very happy with this setup. It’s fast, there are no ads, and I greatly enjoy that my data is mine and mine only.</p>\n\n<h2 id=\"gmail--protonmail\">Gmail ➞ ProtonMail</h2>\n\n<p>If I was to stop relying on Google services, I needed to work on the one that contained most of my life: Gmail. I decided to go with <a href=\"https://protonmail.com/\" target=\"blank\">ProtonMail</a> for multiple reasons: their services are hosted in Switzerland, their products are open-source, and messages are stored with zero-access encryption, as any good online service provider should do. I’m very happy to pay for ProtonMail Plus. Only good experiences with it 4 years in.</p>\n\n<p>Getting started is easy: set up a forwarding rule in Gmail settings so that any emails sent to your Gmail address get forwarded to your ProtonMail address. This way, no need to even have to monitor Gmail account anymore: it’s all going to the ProtonMail address.</p>\n\n<p>The tricky thing however is to change email addresses everywhere: online services, offline businesses, friends and family, etc. Patience is key for this one. I’m still waiting to delete my Gmail account because to this day I receive the odd email to the Gmail address every now and then, though this is becoming less frequent as I’m changing email addresses every time I get those odd emails directly to Gmail. I created a label in ProtonMail for emails that are sent to the Gmail address, so directly in the inbox I can tell when an email was sent directly to the Gmail address (meaning I should let the sender know I changed my email address).</p>\n\n<p>Additionally, if you’d like to migrate the old emails you have in your Gmail account to ProtonMail, you may use Google’s export service <a href=\"https://takeout.google.com\" target=\"blank\">Google Takeout</a>, which lets you download an <code class=\"language-plaintext highlighter-rouge\">.mbox</code> file you can then import into ProtonMail.</p>\n\n<h2 id=\"google-calendar--tasks--radicale-self-hosted\">Google Calendar &amp; Tasks ➞ Radicale (self-hosted)</h2>\n\n<p>Good. Gmail is out of the way. But the other big thing I relied on Google for was calendars and tasks. How does one move years of calendar data to another service, and most importantly, which one?</p>\n\n<p>I decided to go with <a href=\"https://radicale.org/\" target=\"blank\">Radicale</a>, a “Free and Open-Source CalDAV and CardDAV Server” I consider in the “set-and-forget” category. I genuinely never bothered with debugging Radicale even 3 years after setting it up. It’s that good.</p>\n\n<p>Radicale is only the “server” part of the setup, meaning it provides a CalDAV API for clients to use to create/update/delete calendar events. To actually manage your calendar, you need to use a client to connect to your Radicale server.</p>\n\n<p>I installed Radicale on my Raspberry Pi 3B+, which, as mentioned earlier, has a static IP on my local area network (LAN). This means that the Raspberry Pi is always available at the same IP address even when my router reboots. Thanks to this, I can point clients on my Android smartphone and on my Linux laptop to that IP address knowing they’ll always be able to talk to Radicale.</p>\n\n<p>On Android, I use <a href=\"https://www.davx5.com/\" target=\"blank\">DAVx⁵</a> (previously known as DAVdroid) to connect to the Radicale server (punch in the IP address, username and password, and you’re done), and <a href=\"https://f-droid.org/en/packages/ws.xsoh.etar/\" target=\"blank\">Etar</a> to actually interact with my calendar (add/update/delete events). I also use <a href=\"https://icsx5.bitfire.at/\" target=\"blank\">ICSx⁵</a> to read external calendar files (for PagerDuty on-call shifts, for example) so that they show up in Etar. For tasks, I use <a href=\"https://f-droid.org/packages/org.dmfs.tasks/\" target=\"blank\">OpenTasks</a>, which just like Etar also talks to DAVx⁵.</p>\n\n<p>On my laptop, I simply use <a href=\"https://www.thunderbird.net/\" target=\"blank\">Thunderbird</a> with the <a href=\"https://www.thunderbird.net/en-US/calendar/\" target=\"blank\">Lightning</a> extension. Doesn’t get any easier than that.</p>\n\n<p>What this provides is a LAN-only set up, meaning my calendar and tasks data are only stored on my Raspberry Pi, Android phone, and Linux laptop. Given that I nearly always come back home (on my local network) at least once a day, I know my data will be synchronized daily. This setup might be a problem however if/when I leave home for an extended period of time with both my laptop and my smartphone, because they won’t have the Radicale server on the Raspberry Pi to communicate with anymore, leaving both my devices out of sync until I come back home. This hasn’t been a problem for me yet, but certainly something to keep in mind.</p>\n\n<h2 id=\"google-drive--syncthing-self-hosted\">Google Drive ➞ Syncthing (self-hosted)</h2>\n\n<p>I never really used Google Drive for much, but I did have one very important file in there that I used across multiple devices: my KeePass .kdbx database (in an encrypted archive). I removed everything I had in Google Drive, opted for a local-first approach, and started using <a href=\"https://syncthing.net/\" target=\"blank\">Syncthing</a> to selectively share what I need. Thanks to Syncthing, I can directly share my KeePass database between my Android smartphone and Linux laptop without needing a third-party to sync anything. I find the simplicity of this setup quite elegant. To access and update my KeePass database, I use <a href=\"https://keeweb.info/\" target=\"blank\">KeeWeb</a> on my laptop and <a href=\"https://github.com/PhilippC/keepass2android\" target=\"blank\">Keepass2Android</a> on my Android device.</p>\n\n<p>For occasional file server requirements (say, downloading a PDF on my Kobo e-reader using its web browser), I have two options: I set up nginx on the Raspberry Pi to expose a specific directory as an HTTP file server, so I can copy a file to the Raspberry Pi in that directory to make it available to my LAN; alternatively, I sometimes use Termux on my Android device to spin up an HTTP server in a specific directory. Both approaches work nicely, though I have to say there’s something really fun in spinning up a Python SimpleHTTPServer in the command line on my Android device. :)</p>\n\n<h2 id=\"google-photos--external-hard-drives\">Google Photos ➞ external hard drives</h2>\n\n<p>I never was one to upload my photographs to a third-party service (Flickr, Picasa, Google Photos, etc.), so I never actually did such a migration, but I want to point out that it’s a good idea to keep your photos to yourself. Never share more than necessary with untrusted third-parties.</p>\n\n<p>I store photos and videos on a pair of external hard drives that I manually mirror 1:1 (your typical RAID 1 pair). This is a weak part of my data setup, because both drives are in the same physical location. If my apartment building burns down, so do all my photos and videos. I’m looking into creating a third copy to store in a remote location (e.g. a trusted friend/relative’s place) to reduce this risk. I’m not there yet, but the end goal is the 3-2-1 backup strategy: “have 3 copies of your data on two different media with one copy off-site for disaster recovery”.</p>\n\n<h2 id=\"youtube--nextcloud\">YouTube ➞ Nextcloud</h2>\n\n<p>My use case for YouTube is simply sharing videos with friends and family. I’m not a YouTuber that shares content with the entire world, so I don’t need the large-scale capabilities of YouTube, nor its comment and rating system, etc. To power my use case with a privacy-friendly solution, I signed up with <a href=\"https://nextcloud.com/blog/introducing-linuxfabrik-our-new-swiss-based-nextcloud-partner/\" target=\"blank\">Linuxfabrik</a>, a Nextcloud instance hosted in Switzerland (again, for its data privacy laws) with end-to-end encryption. This lets me upload videos and create shareable links with passwords to send to friends and family.</p>\n\n<h2 id=\"google-play-music--spotify\">Google Play Music ➞ Spotify</h2>\n\n<p>I started using Google Play Music soon after it launched, and eventually they shut it down and later turned it into YouTube Music, so I moved to Spotify. No regrets there: <a href=\"https://open.spotify.com/\" target=\"blank\">Spotify</a> has a lot more music, great playlist recommendations, and feels much better than Google Play Music did.</p>\n\n<h2 id=\"google-keep--standard-notes\">Google Keep ➞ Standard Notes</h2>\n\n<p>I used to keep (pun unintended) notes and ideas and thoughts in Google Keep, which was easy to use on Android and okay on the web, but eventually discovered <a href=\"https://standardnotes.com/\" target=\"blank\">Standard Notes</a>, a private and secure (and open-source!) notes app with excellent Web and mobile clients. In fact, I’m writing this blog post in Standard Notes on <a href=\"https://maximevaillancourt.com/blog/why-i-use-a-thinkpad-x220-in-2021\" class=\"internal-link\">my ThinkPad X220</a> using the Standard Notes web UI.</p>\n\n<p>Again, I’m happy to pay for Standard Notes Extended because I know I’m encouraging a small team of people who care about privacy and long-term data integrity. It feels good to pay for something knowing the folks behind the scenes are doing the right thing.</p>\n\n<p>While I <em>could</em> self-host Standard Notes (<a href=\"https://docs.standardnotes.org/self-hosting/getting-started/\" target=\"blank\">they have great documentation explaining how to do that</a>), I enjoy having access to my notes from anywhere in the world, so I’m sticking to their hosted solution for now.</p>\n\n<h2 id=\"google-maps--osmand\">Google Maps ➞ OsmAnd</h2>\n\n<p>This one, I have to say, I struggle to stick to. Google Maps is just that good. The search feature works really well, and time estimates (with traffic considered) are accurate. As much as I’d like to jump to OsmAnd full-time and delete Google Maps from my Android device, I find that <a href=\"https://osmand.net/\" target=\"blank\">OsmAnd</a>’s search feature doesn’t cut it, and its UI is somewhat confusing. One day! For now, sticking to a combination of the two.</p>\n\n<p>Most importantly: whatever app I use, I always keep location services off unless I actively need them.</p>\n\n<h2 id=\"android--somewhat-degoogled-android\">Android ➞ (somewhat DeGoogle’d) Android</h2>\n\n<p>Another big one. While the Android distributions we run on our devices are based on the <a href=\"https://source.android.com/\" target=\"blank\">Android Open Source Project</a> (AOSP)–which is open to everyone–Google still has a lot of influence on the direction of the operating system and how it connects to Google services. I previously used a custom ROM on my Android device without Google Play Services (though with microG), but I eventually went back to a stock ROM (with Google Play Services) to ensure full functionality with PagerDuty, which I rely on for work purposes. I had a hard time imagining myself justifying why I missed a page with “well I use a custom ROM on my Android phone and somehow the telephony firmware was messed up so I missed all the calls and notifications stopped working so this is why everything’s been down for 2 hours”.</p>\n\n<p>Still, I don’t have a Google account connected on the phone, so no data being shared with Google there, and I uninstalled all Google-powered apps (except Google Maps as mentioned above). I use F-Droid to install apps, and Aurora Store for the occasional non-free app (such as PagerDuty).</p>\n\n<h2 id=\"google-search--duckduckgo\">Google Search ➞ DuckDuckGo</h2>\n\n<p><a href=\"https://duckduckgo.com/\" target=\"blank\">DuckDuckGo</a>. Enough said. :)</p>\n\n<h2 id=\"google-authenticator--\">Google Authenticator ➞ ?</h2>\n\n<p>All my two-factor authentication codes are currently stored in the Google Authenticator app on my Android device. I intend to move to another solution soon; just need to take the time to actually do it. Still not sure about which solution to pick yet.</p>\n\n<h2 id=\"google-chrome--mozilla-firefox\">Google Chrome ➞ Mozilla Firefox</h2>\n\n<p>This one is quite obvious. Download <a href=\"https://www.mozilla.org/en-US/firefox/\" target=\"blank\">Mozilla Firefox</a> (on mobile too!), install, and enjoy. Consider reading <a href=\"https://www.quippd.com/writing/2021/07/26/firefox-privacy-stop-hardening-love-strict-etp.html\" target=\"blank\">this blog post from Asif Youssuff about privacy in Firefox</a> to improve your experience, and make sure to install an ad content blocker such as <a href=\"https://addons.mozilla.org/en-US/firefox/addon/ublock-origin/\" target=\"blank\">uBlock Origin</a>.</p>\n\n<hr>\n\n<p>This is the result of a few years of thinking about agency over my data, about long-term data integrity failure scenarios, and data privacy in general.</p>\n\n<p>Next step: potentially introduce <a href=\"https://tailscale.com/\" target=\"blank\">Tailscale</a> into the mix to safely allow access to the devices on my LAN from anywhere in the world.</p>\n\n<p>Next next step: finally delete that Google account. 🔥</p>\n",
            "url": "https://maximevaillancourt.com/blog/moving-away-from-google-services-8-years-in",
            "summary": "Self-hosted LAN-only services FTW!",
            "date_published": "2021-07-28 00:00:00 +0000"
            
        },

        {
            "id": "/blog/canon-dslr-webcam-debian-ubuntu",
            "title": "Using a Canon DSLR as a webcam with Debian/Ubuntu",
            "content_html": "<p>The following was tested with a Canon EOS 6D and a ThinkPad X220 running Debian 10 Buster on Linux Kernel 4.19.0-9-amd64. Your mileage may vary depending on your particular configuration, but I expect this to work on a wide range of Canon DSLRs and other Linux distributions.</p>\n\n<p><strong>1. Install and set up dependencies</strong></p>\n\n<p>First, we need to install a few packages.</p>\n\n<div class=\"language-sh highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ </span><span class=\"nb\">sudo </span>apt-get <span class=\"nb\">install </span>gphoto2 v4l2loopback-utils v4l2loopback-dkms ffmpeg build-essential libelf-dev linux-headers-<span class=\"si\">$(</span><span class=\"nb\">uname</span> <span class=\"nt\">-r</span><span class=\"si\">)</span> unzip vlc\n</code></pre></div></div>\n\n<p><strong>2. Connect your camera</strong></p>\n\n<p>Use the Mini-USB to USB-A cable to connect the DSLR to your computer. Then, turn the camera on, and make sure it’s dialed to Photo mode (in Photo mode, I’m able to get ~20fps, but in Video mode, I barely get 4fps).</p>\n\n<p><strong>3. Start the capture</strong></p>\n\n<p>One last step before we can start capturing: we need to enable the v4l2 kernel module.</p>\n\n<div class=\"language-sh highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ </span><span class=\"nb\">sudo </span>modprobe v4l2loopback\n</code></pre></div></div>\n\n<p>Now, let’s start the capture using gphoto2 and ffmpeg.</p>\n\n<div class=\"language-sh highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ </span>gphoto2 <span class=\"nt\">--stdout</span> <span class=\"nt\">--capture-movie</span> | ffmpeg <span class=\"nt\">-i</span> - <span class=\"nt\">-vcodec</span> rawvideo <span class=\"nt\">-pix_fmt</span> yuv420p <span class=\"nt\">-threads</span> 0 <span class=\"nt\">-f</span> v4l2 /dev/video2\n</code></pre></div></div>\n\n<p>You may need to tweak the last argument to a different device depending on your configuration. Try using <code class=\"language-plaintext highlighter-rouge\">/dev/video0</code>, <code class=\"language-plaintext highlighter-rouge\">/dev/video1</code>, etc. if <code class=\"language-plaintext highlighter-rouge\">/dev/video2</code> doesn’t work.</p>\n\n<p><strong>4. Watch the feed</strong></p>\n\n<div class=\"language-sh highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>vlc v4l2:///dev/video2\n</code></pre></div></div>\n\n<p>(again, make sure to use the appropriate device)</p>\n\n<p>If you see the image from your camera, you’re all set! You should now be able to use this video feed with video conference software (Jitsi Meet, Google Meet, etc.).</p>\n\n<p>Enjoy!</p>\n",
            "url": "https://maximevaillancourt.com/blog/canon-dslr-webcam-debian-ubuntu",
            "summary": "No EOS Webcam Utility on Linux? No problem!",
            "date_published": "2021-01-04 00:00:00 +0000"
            
        },

        {
            "id": "/blog/optimizing-server-side-storefront-rendering-shopify",
            "title": "Optimizing server-side storefront rendering at Shopify",
            "content_html": "<p><em>This post <a href=\"https://shopify.engineering/simplify-batch-cache-optimized-server-side-storefront-rendering\" target=\"blank\">originally appeared on the Shopify Engineering blog</a> on December 10, 2020. I co-wrote it with my colleague Celso Dantas, Staff Developer at Shopify.</em></p>\n\n<div style=\"position: relative; width: 100%; padding-bottom: 56.25%;\"><iframe style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;\" src=\"https://www.youtube.com/embed/LuqoHA9_k8w\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"\"></iframe></div>\n\n<p>In the previous post about <a href=\"https://shopify.engineering/how-shopify-reduced-storefront-response-times-rewrite\" target=\"blank\" title=\"How Shopify Reduced Storefront Response Times with a Rewrite\" rel=\"nofollow noopener noreferrer\">Shopify's new storefront rendering engine</a>, we described how we went about the rewrite process and smoothly transitioned to serve storefront requests with the new implementation. As a follow-up and based on readers’ comments and questions, this post dives deeper into the technical details of how we built the new storefront rendering engine to be faster than the previous implementation.</p>\n<p>To set the table, let’s see how the new storefront rendering engine performs:</p>\n<ul>\n<li>It generates a response in less than ~45ms for 75% of storefront requests;</li>\n<li>It generates a response in less than ~230ms for 90% of storefront requests;</li>\n<li>It generates a response in less than ~900ms for 99% of storefront requests.</li>\n</ul>\n<p>Thanks to the new storefront rendering engine, the average storefront response is nearly 5x faster than with the previous implementation. Of course, how fast the rendering engine is able to process a request and spit out a response depends on two key factors: the shop’s Liquid theme implementation, and the number of resources needed to process the request. To get a better idea of where the storefront rendering engine spends its time when processing a request, try using the <a href=\"https://shopify.engineering/in-depth-liquid-render-analysis-shopify-theme-inspector-chrome-extension\" target=\"blank\" title=\"How to Do an In-depth Liquid Render Analysis with Theme Inspector\" rel=\"nofollow noopener noreferrer\">Shopify Theme Inspector</a>: this tool will help you identify potential bottlenecks so you can work on improving performance in those areas.</p>\n\n<p><img alt=\"A data scheme diagram showing that the Storefront Renderer and Redis instance are contained in a Kubernetes node. The Storefront Renderer sends Redis data. The Storefront Renderer sends data to two sharded data stores outside of the Kubernetes node: Sharded MySQL and Sharded Redis\" src=\"https://cdn.shopify.com/s/files/1/0779/4361/files/Storefront-renderer-data-schema_c5f379b7-619f-4ddb-8064-d093550c4731.jpg?v=1607636250\"></p>\n\n<div class=\"faded\" style=\"text-align: center;\">A simplified data schema of the application</div>\n\n<p>Before we cover each topic, let’s briefly describe our application stack. As mentioned in the previous post, the new storefront rendering engine is a Ruby application. It talks to a sharded MySQL database and uses Redis to store and retrieve cached data.</p>\n<p>Optimizing how we load all that data is extremely important. As one of our requirements was to improve rendering time for Storefront requests. Here are some of the approaches that we took to accomplish that.</p>\n<h2>Using MySQL’s multi-statement feature to reduce round trips</h2>\n<p>To reduce the number of network round trips to the database, we use <a href=\"https://dev.mysql.com/doc/internals/en/multi-statement.html\" target=\"blank\" title=\"MySQL - 14.8.2 Multi-Statement\" rel=\"nofollow noopener noreferrer\">MySQL’s multi-statement feature</a> to allow sending multiple queries at once. With a single request to the database, we can load data from multiple tables at once. Here’s a simplified example:</p>\n<figure>\n<script src=\"https://gist.github.com/ShopifyEng/493da2dbb6d7fd5da082175ff4dcaa2b.js\"></script>\n</figure>\n<p>This request is especially useful to batch-load a lot of data very early in the response lifecycle based on the incoming request. After identifying the type of request, we trigger a single multi-statement query to fetch the data we need for that particular request in one go, which we’ll discuss later in this blog post. For example, for a request for a product page, we’ll load data for the product, its variants, its images, and other product-related resources in addition to information about the shop and the storefront theme, all in a single round-trip to MySQL.</p>\n<h2>Implementing a thin data mapping layer</h2>\n<p>As shown above, the new storefront rendering engine uses handcrafted, optimized SQL queries. This allows us to easily write fine-tuned SQL queries to select only the columns we need for each resource and leverage JOINs and sub-SELECT statements to optimize data loading based on the resources to load which are sometimes less straightforward to implement with a full-service object-relational mapping (ORM) layer.</p>\n<p>However, the main benefit of this approach is the tiny memory footprint of using a raw MySQL client compared to using an object-relational mapping (ORM) layer that’s unnecessarily complex for our needs. Since there’s no unnecessary abstraction, forgoing the use of an ORM drastically simplifies the flow of data. Once the raw rows come back from MySQL, we effectively use the simplest ORM possible: we create plain old Ruby objects from the raw rows to model the business domain. We then use these Ruby objects for the remainder of the request. Below is an example of how it’s done.</p>\n<p>\n<script src=\"https://gist.github.com/ShopifyEng/720c3f01ac92e47621f2f383700b9498.js\"></script>\n</p>\n<p>Of course, not using an ORM layer comes with a cost: if implemented poorly, this approach can lead to more complexity leaking into the application code. Creating thin model abstractions using plain old Ruby objects prevents this from happening, and makes it easier to interact with resources while meeting our performance criteria. Of course, this approach isn’t particularly common and has the potential to cause panic in software engineers who aren’t heavily involved in performance work, instead worrying about schema migrations and compatibility issues. However, when speed is critical, we accept to take on that complexity.</p>\n<h2>Book-keeping and eager-loading queries</h2>\n<p>An HTTP request for a Shopify storefront may end up requiring many different resources from data stores to render properly. For example, a request for a product page could lead to requiring information about other products, images, variants, inventory information, and a whole lot of other data not loaded on multi-statement select. The first time the storefront rendering engine loads this page, it needs to query the database, sometimes making multiple requests, to retrieve all the information it needs. This usually happens during the request at any given time.</p>\n<p><img alt=\"A flow diagram showing the Storefront Renderer's requests from  the data stores and how it uses a Query Book Keeper Middlewear to eager-load data\" src=\"https://cdn.shopify.com/s/files/1/0779/4361/files/flow-request-bookeeping-solution_adbd68eb-cc30-4be5-9bdf-104011224ad2.jpg?v=1607636269\"></p>\n<div class=\"faded\" style=\"text-align: center\">Flow of a request with the book-keeping solution</div>\n<p>As it retrieves this data for the first time, the storefront rendering engine keeps track of the queries it performed on the database for that particular product page and stores that list of queries in a key-value store for later use. When an HTTP request for the same product page comes in later (which it knows when the cache key matches), the rendering engine looks up the list of queries it performed throughout the previous request of the same type and performs those queries all at once, at the very beginning of the current request, because we’re pretty confident we’ll need them for this request (since they were used in the previous request).</p>\n<p>This book-keeping mechanism lets us eager-load data we’re pretty confident we’ll need. Of course, when a page changes, this may lead to over-fetching and/or under-fetching, which is expected, and the shape of the data we fetch stabilizes quickly over time as more requests come in.</p>\n<p>On the other side, some liquid models of Shopify’s storefronts are not accessed as frequently, and we don’t need to eager-load data related to them. If we did, we’d increase I/O wait time for something that we probably wouldn’t use very often. What the new rendering engine does instead is lazy-load this data by default. Unless the book-keeping mechanism described above eager-loads it, we’ll defer retrieving data to only load it if it’s needed for a particular request.</p>\n<h2>Implementing caching layers</h2>\n<p>Much like a CPU’s caching architecture, the new rendering engine implements multiple layers of caching to accelerate responses.</p>\n<p>A critical aside before we jump into this section: adding caching should never be the first step towards building performance-oriented software. Start by building a solution that’s extremely fast from the get go, even without caching. Once this is achieved, then consider adding caching to reduce load on the various components on the system while accelerating frequent use cases. Caching is like a sharp knife and can introduce hard to detect bugs.</p>\n<h3>In-memory cache</h3>\n<p><img alt=\"A data scheme diagram showing that the Storefront Renderer and Redis instance are contained in a Kubernetes node. Within the Storefront Renderer is an In-memory cache. The Storefront Renderer sends Redis data. The Storefront Renderer sends data to two sharded data stores outside of the Kubernetes node: Sharded MySQL and Sharded Redis\" src=\"https://cdn.shopify.com/s/files/1/0779/4361/files/Storefront-renderer-data-schema-in-memory-cache.jpg?v=1607636398\"></p>\n<div class=\"faded\" style=\"text-align: center;\">The same schema as above, with the in-memory cache this time</div>\n<p>At the frontline of our caching system is an in-memory cache that you can essentially think of as a global hash that’s shared across requests within each web worker. Much like the majority of our caching mechanisms, this caching layer uses the LRU caching algorithm. As a result, we use this caching layer for data that’s accessed very often. This layer is especially useful in high throughput scenarios such as flash sales.</p>\n<h3>Node-local shared caching</h3>\n<p>As a second layer on top of the in-memory cache, the new rendering engine leverages a node-local Redis store that’s shared across all server workers on the same node. Since the database is available on the same machine as the rendering engine process itself, this node-local data transfer prevents network overhead and improves response times. As a result, multiple Ruby processes benefit from sharing cached data with one another.</p>\n<h3>Full-page caching</h3>\n<p>Once the rendering engine successfully renders a full storefront response for a particular type of request, we store the final output (most often an HTML or JSON string) into the local Redis for later retrieval for subsequent requests that match the same cache key. This full-page caching solution lets us prevent regenerating storefront responses if we can by using the output we previously computed.</p>\n<h3>Database query results caching</h3>\n<p>In a scenario where the full-page output cache, the in-memory cache, and the node-local cache doesn’t have a valid entry for a given request, we need to reach all the way to the database. Once we get a result back from MySQL, we transparently cache the results in Redis for later retrieval based on the queries and their parameters. As long as the cache keys don’t change, running the same database queries over and over always hit Redis instead of reaching all the way to the database.</p>\n<h3>Liquid object memoizer</h3>\n<p>Thanks to the Liquid templating language, merchants and partners may build custom storefront themes. When loading a particular storefront page, it’s possible that the Liquid template to render includes multiple references to the same object. This is common on the product page for example, where the template will include many references to the product object: <code>{{ product.title }}</code>, <code>{{ product.description }}</code>, <code>{{ product.featured_media }}</code>, and others.</p>\n<p>Of course, when each of these are executed, we don’t fetch the product over and over again from the database—we fetch it once, then keep it in memory for later use throughout the request lifecycle. This means that if the same product object is required multiple times at different locations during the render process, we’ll always use the same one and only instance of it throughout the entire request lifecycle.</p>\n<p>The Liquid object memoizer is especially useful when multiple different Liquid objects end up loading the same resource. For example, when loading multiple product objects on a collection page using <code></code> and then referring to a particular product using <code>{{ all_products['cowboy-hat'] }}</code> on a collection page, with the Liquid object memoizer we’ll load it from an external data store once, then store it in memory and fetch it from there if it’s needed later. On average, across all Shopify storefronts, we see that the Liquid object memoizer prevents between 16 and 20 accesses to Redis and/or MySQL for every single storefront request, where we leverage the in-memory cache instead. In some extreme cases, we see that the memoizer prevents up to 4,000 calls to data stores per request.</p>\n<h2>Reducing memory allocations</h2>\n<h3>Writing memory-aware code</h3>\n<p>Garbage collection execution is expensive. So we write code that doesn’t generate unnecessary objects. Use of methods and algorithms that modify objects in place, instead of generating a new object. For example:</p>\n<ul>\n<li>use <code>#map!</code> instead of <code>#map</code> when dealing with lists. It prevents a new Array object from being created.</li>\n<li>Use string interpolation instead of string concatenation. Interpolation does not create intermediate unnecessary String objects.</li>\n</ul>\n<p>This may not seem like much, but consider this: using <code>#map!</code> instead of <code>#map</code> could reduce your memory usage significantly, even when simply looping over an array of integers to double the values.</p>\n<p>Let’s set up an following array of 1000 integers from 1 to 1000:</p>\n<p style=\"padding-left: 30px;\"><code>array = (1..1000).to_a</code></p>\n<p>Then, let’s double each number in the array with Array#map:</p>\n<p style=\"padding-left: 30px;\"><code>array.map { |i| i * 2 }</code></p>\n<p>The line above leads to one object allocated in memory, for a total of 8040 bytes.</p>\n<p>Now let’s do the same thing with <code>Array#map!</code> instead:</p>\n<p style=\"padding-left: 30px;\"><code>array.map! { |i| i * 2 }</code></p>\n<p>The line above leads to zero object allocated in memory, for a total of 0 bytes.</p>\n<p>Even with this tiny example, using map! instead of map saves ~8 kilobytes of allocated memory, and considering the sheer scale of the Shopify platform and the storefront traffic throughput it receives, every little bit of memory optimization counts to help the garbage collector run less often and for smaller periods of time, thus improving server response times.</p>\n<p>With that in mind, we use tracing and profiling tools extensively to dive deeper into areas in the rendering engine that are consuming too much memory and to make precise changes to reduce memory usage.</p>\n\n<p><em>Post-publication addendum: the <a href=\"https://github.com/rubocop-hq/rubocop-performance\" target=\"blank\">Rubocop team offers a set of performance-oriented cops</a> that highlight potential performance improvements - I highly recommend trying them out!</em></p>\n\n<h3>Method-specific memory benchmarking</h3>\n<p>To prevent accidentally increasing memory allocations, we built a test helper method that lets us benchmark a method or a block to know many memory allocations and allocated bytes it triggers. Here’s how we use it:</p>\n<figure>\n<script src=\"https://gist.github.com/ShopifyEng/06f1af6bc007cf78309acd9e9f55c13b.js\"></script>\n</figure>\n<p>This benchmark test will succeed if calling <code>Product.find_by_handle('cowboy-hat')</code> matches the following criteria:</p>\n<ul>\n<li>The call allocates between 48 and 52 objects in memory;</li>\n<li>The call allocates between 5100 and 5200 bytes in memory.</li>\n</ul>\n<p>We allow a range of allocations because they’re not deterministic on every test run. This depends on the order in which tests run and the way data is cached, which can affect the final number of allocations.</p>\n<p>As such, these memory benchmarks help us keep an eye on memory usage for specific methods. In practice, they’ve prevented introducing inefficient third-party gems that bloat memory usage, and they’ve increased awareness of memory usage to developers when working on features.</p>\n<p>We covered three main ways to improve server-side performance: batching up calls to external data stores to reduce roundtrips, caching data in multiple layers for specific use cases, and simplifying the amount of work required to fulfill a task by reducing memory allocations. When they’re all combined, these approaches lead to big time performance gains for merchants on the platform—the average response time with the new rendering engine is 5x faster than with the previous implementation. </p>\n<p>Those are just some of the techniques that we are using to make the new application faster. And we never stop exploring new ways to speed up merchant’s storefronts. Faster rendering times are in the DNA of our team!</p>\n\n<p><em>This blog post is <strong>not</strong> available under a Creative Commons license.</em></p>\n",
            "url": "https://maximevaillancourt.com/blog/optimizing-server-side-storefront-rendering-shopify",
            "summary": "Simplify, batch, and cache.",
            "date_published": "2020-12-20 00:00:00 +0000"
            
        },

        {
            "id": "/blog/monitoring-a-grain-dryer-via-the-internet",
            "title": "Monitoring a grain dryer via the Internet",
            "content_html": "<p>A relative of mine called a few months ago: “Hey, I have a grain dryer that I’d like to monitor from my phone. Is that something you could set up in time for harvest season?”</p>\n\n<p>I have no idea how grain dryers work, especially not Internet-enabled grain dryers — because <em>of course</em> that’s a thing — but I figured I’d give it a shot anyway.</p>\n\n<p>For context, a grain dryer is literally a combination of a huge furnace and fans that, you guessed it, dries harvested grain for optimal (lack of) moisture.</p>\n\n<div class=\"img-group\">\n<div class=\"\">\n  <img alt=\"A GSI grain dryer next to a grain silo.\" src=\"/assets/dryer/gsi-1220.jpg\">\n</div>\n<div class=\"caption\">\n  A GSI grain dryer next to a grain silo. Image © <a href=\"https://midwestagsystems.net/\" target=\"blank\">Midwest AG Systems</a>\n</div>\n</div>\n\n<p>Such dryers typically have an embedded computer running some version of Windows and a touchscreen display that allows monitoring and controlling the dryer’s temperature, timers, and other parameters to get the perfect output.</p>\n\n<div class=\"img-group\">\n<div class=\"\">\n  <img alt=\"A GSI grain dryer's control panel.\" src=\"/assets/dryer/controller.png\">\n</div>\n<div class=\"caption\">\n  A GSI grain dryer's control panel. Image © <a href=\"https://www.grainsystems.com/master/products/conditioning/watchdog-technology.html\" target=\"blank\">GSI</a>\n</div>\n</div>\n\n<p>However, because these things run for hours at a time, and because agriculture workers during harvest season usually end up in a sleep-deprived state that lasts for multiple weeks, having the ability to monitor these grain dryers remotely lets workers get more precious sleep time (instead of having to walk/drive up to the grain dryer to see if everything’s okay in the middle of the night).</p>\n\n<p>The desired outcome is to monitor the grain dryer remotely using the web interface:</p>\n\n<div class=\"img-group\">\n<div class=\"\">\n  <img alt=\"An iPhone with the grain dryer's web monitoring interface open in the browser.\" src=\"/assets/dryer/web-ui.jpg\">\n</div>\n<div class=\"caption\">\n  The web interface to monitor the grain dryer's current state. Image © <a href=\"https://www.grainsystems.com/content/dam/Brands/GSI/Brochures/Conditioning/gs013_Modular-TSeries-Dryers-2017_7.pdf/_jcr_content/renditions/original\" target=\"blank\">GSI</a>\n</div>\n</div>\n\n<p>It’s now harvest season 2020, so I connected a grain dryer to the Internet yesterday. It was fun (and weird) and I learned a few things. Here’s how we did it.</p>\n\n<h3 id=\"what-youll-need\">What you’ll need</h3>\n\n<ul>\n  <li>A GSI grain dryer with a <a href=\"https://www.grainsystems.com/master/products/conditioning/watchdog-technology.html\" target=\"blank\">Watchdog module</a> installed (I was lucky that the dryer already had this module installed)</li>\n  <li>A cellular modem (we purchased a <a href=\"https://www.netgear.com/home/products/mobile-broadband/lte-modems/LB1120.aspx\" target=\"blank\">Netgear LB1120</a>)</li>\n  <li>A cellular data plan with a public static IP (we went with Telus, a Canadian cellular carrier, and needed to open a business account to get access to a public static IP for 15$/month per month on top of the base data plan)</li>\n  <li>A laptop with an Ethernet port</li>\n  <li>A few Ethernet cables</li>\n</ul>\n\n<h3 id=\"topology\">Topology</h3>\n\n<p>Here’s how the network topology looks like:</p>\n\n<div class=\"highlighter-rouge\" style=\"width: min-content;margin: 0 auto;\"><div class=\"highlight\"><pre class=\"highlight\" style=\"width: auto; white-space: pre;\"><code>                   (via public static IP)\n                        +----------+\n         +--------------&gt; Internet &lt;--------------+\n         |              +----------+              |\n +-------v--------+                       +-------v-------+\n | Cellular modem |                       | Mobile device |\n +-------^--------+                       +---------------+\n         |\n+--------v----------+\n|    Grain dryer    |\n| (Watchdog module) |\n+-------------------+\n</code></pre></div></div>\n\n<p>Essentially, we’ll expose the grain dryer’s web interface to the Internet via a public static IP using a cellular modem. Once it’s exposed, the operator will be able to navigate to that static IP from their mobile device to access the grain dryer’s web interface.</p>\n\n<h3 id=\"subscribing-for-a-data-plan-and-a-static-ip\">Subscribing for a data plan and a static IP</h3>\n\n<p>I called various cellular carriers here in Canada to learn more about their data-only plans and the possibility of adding a public static IP option to the plan: the only carrier that seemed like they knew what they were talking about was Telus. Others either didn’t offer the public static IP option, or didn’t know what I was talking about.</p>\n\n<p>With that, we went ahead and signed up for a business account at Telus (required to get access to the public static IP option), bought a SIM card, and a data-only plan for the modem.</p>\n\n<h3 id=\"setting-up-the-cellular-modem\">Setting up the cellular modem</h3>\n\n<p>First up, we need to make sure we’re able to connect to the nearest cellular tower to connect to the Internet. The Netgear LB1120 is fairly easy to set up on paper: pop a SIM card in there and we’re done, right? Well, for our use case, not quite. There are a few things to do to set it up to use the static IP assigned to the data plan from the cellular carrier. More importantly, the modem needs to be set up in “bridge” mode (instead of “router” mode) to act as a simple bridge (rather than a router) to connect it directly to the grain dryer.</p>\n\n<div class=\"img-group\">\n<div class=\"overflow\">\n  <img alt=\"Laptop, modem, cables, and various manuals spread out on concrete.\" src=\"/assets/dryer/setup-modem.jpg\">\n</div>\n<div class=\"caption\">\n  On-premise setup to configure the modem and the dryer. That's my trusty ThinkPad X220.\n</div>\n</div>\n\n<p>Days before arriving on the premises, <a href=\"https://community.netgear.com/t5/Mobile-Routers-Hotspots-Modems/LB1120-Bridge-Mode-No-Connectivity/m-p/1404666#M3431\" target=\"blank\">I read online that the firmware that ships out of the box is broken</a>: “bridge” mode doesn’t function at all, and an update is required to fix it. This made me a bit nervous because I didn’t have the modem with me, and wasn’t sure if I was going to be able to get the update to work properly. Thankfully, upon inserting the SIM card, turning the modem on, plugging it into the computer via an Ethernet cable, and accessing the modem’s setup interface at <code class=\"language-plaintext highlighter-rouge\">http://192.168.5.1</code>, the modem’s web UI suggests downloading the latest firmware over the air. Neat!</p>\n\n<p>From what I read, you must use a firmware more recent than <code class=\"language-plaintext highlighter-rouge\">NTG9X07C_12.09.05.27</code> to make sure “bridge” mode works fine. In our case, we updated to <code class=\"language-plaintext highlighter-rouge\">NTG9X07C_12.09.05.30</code>, so we’re good.</p>\n\n<div class=\"img-group\">\n<div class=\"\">\n  <img alt=\"Updating the modem's firmware over the air.\" src=\"/assets/dryer/firmware-update.jpg\">\n</div>\n<div class=\"caption\">\n  Updating the modem's firmware over the air.\n</div>\n</div>\n\n<p>Once the update completes and the modem reboots, we can go in the settings and change the operation mode to “bridge”:</p>\n\n<div class=\"img-group\">\n<div class=\"\">\n  <img alt=\"Changing the modem's operation mode to Bridge mode.\" src=\"/assets/dryer/bridge-mode.png\">\n</div>\n<div class=\"caption\">\n  Changing the modem's operation mode to Bridge mode. Image © <a href=\"https://kb.netgear.com/31163/How-to-change-4G-LTE-Modem-from-router-mode-to-bridge-mode\" target=\"blank\">Netgear</a>\n</div>\n</div>\n\n<p>Doing so “turns off the router function of the device and assigns the network IP address directly to the attached host”, which is exactly what we need to connect the modem directly to the dryer’s Watchdog module. Save and let the modem reboot.</p>\n\n<p>Now, the SIM is in the modem, which has the latest firmware and is in “bridge” mode, but there’s one last thing to fix: the cellular carrier is assigning a dynamic IP to the modem, which isn’t what we want: we should be getting the static IP for the data plan we’re paying for. To resolve this issue, we need to tweak the modem’s access point name (APN).</p>\n\n<p>I got stuck on this issue for around 30 minutes before remembering that APNs are a thing and that I could possibly tweak it in the modem settings. I’m glad I remembered!</p>\n\n<p>By default, the modem auto-detects the APN from the cellular carrier. For most cases, this works fine, but in this particular case, we want to use a different APN that allows us to use the static IP assigned to our data plan. APN settings vary by cellular carrier, and I ended up searching for Telus’ APN settings while on premise. I found <a href=\"https://usatcorp.com/faqs/common-access-point-names-apn-carrier/\" target=\"blank\">USAT Corp’s website to include various APN settings</a>, so I highly recommend trying those values out for your particular cellular carrier.</p>\n\n<p>In our specific case, we needed to use the following settings to be able to get the static IP assigned to the modem:</p>\n\n<ul>\n  <li>Access point name (APN): <code class=\"language-plaintext highlighter-rouge\">staticipeast.telus.com</code> (we’re on the east coast)</li>\n  <li>Authentication: None</li>\n  <li>Packet data profile (PDP): IPv4</li>\n</ul>\n\n<div class=\"img-group\">\n<div class=\"\">\n  <img alt=\"Configuring the APN to use the static IP assigned to the account.\" src=\"/assets/dryer/apn-settings.png\">\n</div>\n<div class=\"caption\">\n  In the modem settings, we need to configure a new APN to ask the cellular carrier to assign the account's static IP to the modem.\n</div>\n</div>\n\n<p>The LB1120 has a setting to automatically connect to the Internet upon booting: I recommend turning this on to have it connect automatically in the case of a power failure.</p>\n\n<p>Let’s reboot the modem one more time. At this point, the modem should be using the static IP assigned to your data plan by the cellular carrier.</p>\n\n<div class=\"img-group\">\n<div class=\"overflow\">\n  <img alt=\"Signal is equivalent to 2 bars out of 5.\" src=\"/assets/dryer/2-bars.jpg\">\n</div>\n<div class=\"caption\">\n  The Netgear LB1120 cellular modem, all set up and ready to go. Even though it sits at 2 bars out of 5, it's plenty for the low throughput use case of reading data from a dryer.\n</div>\n</div>\n\n<p>A tip to know if the modem is configured correctly: with the computer (device A) connected to the modem via Ethernet, use another device (device B) to access the expected static IP address (I used my smartphone). On device A, serve something (anything) on port 80 (I did <code class=\"language-plaintext highlighter-rouge\">sudo python -m SimpleHTTPServer 80</code>), and see if you get that on device B when visiting the static IP address. If you do, then the modem is all set up and ready to go. If not, something’s wrong (invalid APN, incorrect data plan, no public static IP configured by the cellular carrier, some sort of NAT that prevents direct access, etc.).</p>\n\n<p>Now that the modem is configured properly, let’s move on to configuring the grain dryer itself.</p>\n\n<h3 id=\"configuring-the-dryer\">Configuring the dryer</h3>\n\n<p>The process to configure the dryer is fairly similar to configuring the modem: we essentially connect the dryer to the laptop via Ethernet to configure it via a web interface. But first, let’s look inside the dryer’s computer compartment.</p>\n\n<p>It all peels up like an onion: the outermost layer is the protective door to prevent water and debris from hitting switches and the touchscreen, and the middle layer is the computer and touchscreen itself along with a PLC. The Watchdog module is fixed inside the box itself.</p>\n\n<div class=\"img-group\">\n<div class=\"overflow\">\n  <img alt=\"The dryer's computer and PLC internals exposed.\" src=\"/assets/dryer/onion.jpg\">\n</div>\n<div class=\"caption\">\n  The dryer's PLC. The Watchdog module is not pictured (it's fixed on the right).\n</div>\n</div>\n\n<p>When looking inside the box, the Watchdog module is fixed inside and connected to the rest of the PLC via an RS232 serial port. There are two Ethernet ports: one WAN port (the one on the left in the picture), and a LAN port (the one on the right).</p>\n\n<div class=\"img-group\">\n<div class=\"overflow\">\n  <img alt=\"The Watchdog module fixed inside the computer compartment of the dryer.\" src=\"/assets/dryer/watchdog-module.jpg\">\n</div>\n<div class=\"caption\">\n  The Watchdog module. The WAN port is directly connected to the modem.\n</div>\n</div>\n\n<p>To configure the dryer’s network settings, we need to connect the Watchdog’s LAN port to the computer via Ethernet and access the web interface at <code class=\"language-plaintext highlighter-rouge\">http://10.0.0.1/setup</code>. On this screen, you’ll need to click “Configure”, which will bring up the possibility to use DHCP or a static IP address. In our case, we select the “static IP” option.</p>\n\n<div class=\"img-group\">\n<div class=\"\">\n  <img alt=\"The Watchdog network configuration screen, to configure the static IP address used to connect to the dryer.\" src=\"/assets/dryer/network-setup.png\">\n</div>\n<div class=\"caption\">\n  Configuring the network settings on the grain dryer.\n</div>\n</div>\n\n<p>On the next screen, we fill out the following fields:</p>\n\n<ul>\n  <li>Static IP address</li>\n  <li>Subnet mask</li>\n  <li>Gateway</li>\n  <li>DNS1</li>\n  <li>DNS2</li>\n</ul>\n\n<p>Assuming that the static IP address provided by the cellular carrier is <code class=\"language-plaintext highlighter-rouge\">241.2.31.59</code>, we’d fill out the fields as such:</p>\n\n<ul>\n  <li>Static IP address: <code class=\"language-plaintext highlighter-rouge\">241.2.31.59</code> (static IP from the cellular carrier)</li>\n  <li>Subnet mask: <code class=\"language-plaintext highlighter-rouge\">255.255.255.0</code> (pretty standard stuff)</li>\n  <li>Gateway: <code class=\"language-plaintext highlighter-rouge\">241.2.31.1</code> (same as static IP, but last component is <code class=\"language-plaintext highlighter-rouge\">1</code>)</li>\n  <li>DNS1: <code class=\"language-plaintext highlighter-rouge\">241.2.31.1</code> (we use the gateway as the DNS provider)</li>\n  <li>DNS2: <code class=\"language-plaintext highlighter-rouge\">8.8.8.8</code> (this is Google DNS, because why not)</li>\n</ul>\n\n<p>Once that’s done, save the network configuration, and turn off the grain dryer.</p>\n\n<p>You may now connect the modem to the Watchdog module’s WAN port (as pictured a few paragraphs above).</p>\n\n<p>Now, the moment of truth: turn on the dryer. You may need to wait up to 3 minutes for the dryer and Watchdog to boot completely. A red LED should turn on near the WAN port once the Watchdog module is ready.</p>\n\n<p>At this point, try accessing the static IP from your favourite device. <strong>If you did everything right, you should see the Watchdog web interface, and you’re done!</strong> 🎉</p>\n\n<h3 id=\"questions-and-answers\">Questions and answers</h3>\n\n<p>Q: Why didn’t you just run a cable from the home to the dryer?</p>\n\n<p>A: Because the dryer’s way too far from any building with Internet access. If only it had been so simple!</p>\n\n<p>Q: Why pay for a static IP? Why not dynamic DNS or IPv6? Why not TeamViewer?</p>\n\n<p>A: Telus (and others) do carrier-grade network address translation (CG-NAT), so there’s like a double layer of NAT going on. Using a public static IP bypasses this issue. What’s more, the modem itself doesn’t support dynamic DNS. Additionally, the dryer’s computer runs Windows CE .NET 4.2, so no IPv6 nor TeamViewer support. Heck, I even thought about installing ngrok on that thing, but Windows CE being what it is, I couldn’t.</p>\n\n<hr>\n\n<p>Let’s wrap this up with a pair of nostalgia-inducing visuals: it appears that these dryers run on Windows CE .NET 4.2, which was released in April 2003. Time flies!</p>\n\n<p><img src=\"/assets/dryer/ui.jpg\" alt=\"Welcome screen on the dryer's touchscreen featuring a Windows 95-era logo\"></p>\n\n<p><img src=\"/assets/dryer/windows-ce.jpg\" alt=\"Windows CE .NET 4.2 sticker behind the computer panel\"></p>\n",
            "url": "https://maximevaillancourt.com/blog/monitoring-a-grain-dryer-via-the-internet",
            "summary": "Agriculture meets static IPs and cellular APNs.",
            "date_published": "2020-11-08 00:00:00 +0000"
            
        },

        {
            "id": "/blog/turning-my-smartphone-into-a-boring-tool",
            "title": "Turning my smartphone into a boring tool",
            "content_html": "<p>👉 <em>Looking for actionables and screenshots? Scroll to the bottom of this post.</em></p>\n\n<p>I opened my smartphone’s “screen time” app for the first time recently and was somewhat troubled to discover that on a particular day a few weeks ago:</p>\n<ul>\n  <li>I unlocked my phone 64 times;</li>\n  <li>I used my phone for over 3 hours;</li>\n  <li>I received 145 different notifications (!).</li>\n</ul>\n\n<p>My first thoughts upon seeing these numbers: how much of this time could I consider as “time well spent”? How much of it could I redirect towards healthier activities?</p>\n\n<hr>\n\n<p>Let’s open up an imaginary toolbox for a moment. Imagine a screwdriver.</p>\n\n<p>A screwdriver is a tool in its purest form. It does not ask for anything, does not require maintenance, and doesn’t distract from the task at hand. It just sits there, patiently waiting to be picked up when needed.</p>\n\n<p><strong>A tool does its job, then gets out of the way</strong>.</p>\n\n<p>This is the kind of relationship I want with my smartphone. It should be tool, and nothing else. With that in mind, I set out to turn my smartphone into a tool again. This <a href=\"https://maximevaillancourt.com/blog/tech-is-not-an-end-part-1\" class=\"internal-link\">“tool, not distraction” mentality is useful for all kinds of modern technology</a>, not just smartphones. Cal Newport’s “Digital Minimalism” covers this idea pretty well too (<a href=\"/bookshelf/digital-minimalism-cal-newport\" class=\"internal-link\">view my reading notes</a>).</p>\n\n<hr>\n\n<p>Picking up my smartphone happens in one of two mental modes: mindfully, or mindlessly. There are no other possible modes of operation when picking up a smartphone.</p>\n\n<p>Mindful use is the best case scenario. Ideally, every single time I’d pick up my smartphone, it would be to resolve a problem, and I’d put it down the second I’d be done. That’s entirely reasonable, though it’s not always what happens. What’s more, it’s not very realistic.</p>\n\n<p>Let’s focus on improving the “mindless” part: I noticed that when I mindlessly pick up my phone, it’s either because a notification came in, or because I’m bored. Knowing this, I now have a few points of leverage I can use:</p>\n<ul>\n  <li>\n<strong>I can reduce the number of incoming notifications</strong> (reducing the total number of potential unlocks I attempt on the device);</li>\n  <li>\n<strong>I can make it more difficult to mindlessly unlock the device</strong> (leading me to really consider if I want to use it);</li>\n  <li>\n<strong>I can make sure I spend quality time on the device</strong> once it’s unlocked (encouraging me to do something better than falling prey to distracting apps).</li>\n</ul>\n\n<p>To reduce incoming notifications:</p>\n\n<ul>\n  <li>\n<strong>Disable notifications for most everything</strong>. I have notifications enabled for PagerDuty, phone calls, and video calls. Nothing else, not even instant messaging or email. If it’s important, I’ll go look for it myself.</li>\n  <li>\n<strong>Delete social media apps and mindfully using them on the laptop/desktop instead</strong>. I used to scroll through Twitter every day on my smartphone before. I now use it a few times a week on my laptop.</li>\n</ul>\n\n<p>To reduce the number of mindless unlocks:</p>\n\n<ul>\n  <li>\n<strong>Hide notifications entirely from the lock screen</strong>. This is good from a privacy standpoint and a mindfulness standpoint, as it limits the information exposed to strangers while preventing your curiosity from taking over.</li>\n  <li>\n<strong>Disable biometric authentication</strong> such as face and/or fingerprint unlock and instead <strong>use a strong passphrase</strong>. It’s much less convenient, so it acts as a mindfulness check.</li>\n</ul>\n\n<p>To spend quality time on the device once it’s unlocked:</p>\n\n<ul>\n  <li>\n<strong>Showcase useful apps on your home screen</strong>. For example, Duolingo, podcasts, Instapaper, Anki, Calendar, etc. You get the picture: not Facebook and Twitter.</li>\n  <li>\n<strong>Hide time-wasting apps</strong> from the home screen. Even better, delete them altogether! A first great step is hiding them away in a folder of some sort.</li>\n  <li>\n<strong>Enable grayscale display mode</strong> to reduce the enticing effect of the apps icons’ bright colours.</li>\n  <li>\n<strong>Use an alternative launcher</strong> like <a href=\"https://jkuester.github.io/unlauncher/\" target=\"blank\">Unlauncher</a>. You may even customize your launcher if you’re familiar with Java or Kotlin.</li>\n</ul>\n\n<p>All in all, the goal is to make it easier to use the device in a thoughtful, productive fashion than it is to use it for distracting purposes.</p>\n\n<p>After these changes, screen time data for this week shows that on average I unlocked my device 11 times per day (~6x less than before), received 52 notifications per day (~3x less than before), and spent 70 minutes on the device per day (~3x less than before).  ✨</p>\n\n<h3 id=\"screenshots\">Screenshots</h3>\n\n<div style=\"margin: 1em 0; display: grid; grid-gap: 1em; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\">\n  <div style=\"text-align: center;\">\n    <img alt=\"Lock screen on my Android smartphone, showing the current date and time\" src=\"/assets/lockscreen.jpg\">\n    <span class=\"caption\">\n      Lock screen\n    </span>\n  </div>\n  <div style=\"text-align: center;\">\n    <img alt=\"Home screen on my Android smartphone, showing the current time at the top as well as buttons for four apps: a podcasts app, a spaced repetition app, a language learning app, and a calendar app\" src=\"/assets/homescreen.jpg\">\n    <span class=\"caption\">\n      Home screen\n    </span>\n  </div>\n  <div style=\"text-align: center;\">\n    <img alt=\"App drawer on my Android smartphone, showing scrollable list of apps names in white text over black background\" src=\"/assets/app-drawer.jpg\">\n    <span class=\"caption\">\n      App drawer\n    </span>\n  </div>\n</div>\n\n",
            "url": "https://maximevaillancourt.com/blog/turning-my-smartphone-into-a-boring-tool",
            "summary": "Tools do their job then get out of the way.",
            "date_published": "2020-11-03 00:00:00 +0000"
            
        },

        {
            "id": "/blog/github-email-notifications-gmail-filters",
            "title": "Automatically labeling GitHub notification emails with Gmail filters",
            "content_html": "<p>A fair share of my waking hours involves communicating with other people on GitHub to make sure we’re solving the right problems, and that we’re solving them the right way.</p>\n\n<p>As a result, I receive many email notifications about various things that happen on there: <span style=\"color: #e30000;\">direct requests to review a particular piece of code</span>, <span style=\"color: blue;\">feedback on pull requests I’ve opened</span>, <span style=\"color: green;\">pull requests merged by their authors</span>, <span style=\"color: #c6ad16;\">people directly mentioning our username in a comment</span>, <span style=\"color: grey;\">issues closed by their authors</span>, etc. I receive hundreds of emails every single week.</p>\n\n<p>Now here’s the thing: some of these emails are more time-sensitive and/or actionable than others. We should probably address <span style=\"color: #e30000;\">direct requests to review a particular piece of code</span> first, since someone is explicitly asking for our attention and not responding promptly would likely prevent them from shipping something swiftly. Conversely, I’ll want to delete (or at least deprioritize) email notifications communicating that a given <span style=\"color: green;\">pull request was merged by its author</span> - there’s no actionable to derive from it, so in the bin it goes.</p>\n\n<p>However, by default, all these email notifications arrive in our inboxes with the same perceived level of importance, which makes it difficult to identify what I should address next.</p>\n\n<p><strong>This post presents a solution to this problem</strong>: using Gmail filters, we can automatically add labels to GitHub notification emails based on their content. This solution takes less than 10 minutes to implement, and the long-term return on investment is quite appreciable.</p>\n\n<p>Here’s how my inbox looks like with automatic labeling set up:</p>\n\n<p><img src=\"/assets/github-labels.png\" alt=\"\"></p>\n\n<p>Pretty neat, right? Thanks to these labels, I’m able to quickly parse through emails and reach inbox zero every day.</p>\n\n<p>Let’s see how to implement this solution with Gmail.</p>\n\n<h2 id=\"1-download-the-xml-filters-template\">1. Download the XML filters template</h2>\n\n<p>Start by saving the following XML filters template to a file on your device:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>&lt;?xml version='1.0' encoding='UTF-8'?&gt;\n&lt;feed xmlns='http://www.w3.org/2005/Atom' xmlns:apps='http://schemas.google.com/apps/2006'&gt;\n  &lt;title&gt;GitHub filters&lt;/title&gt;\n  &lt;entry&gt;\n    &lt;category term='filter'&gt;&lt;/category&gt;\n    &lt;content&gt;&lt;/content&gt;\n    &lt;apps:property name='hasTheWord' value='Merged into'/&gt;\n    &lt;apps:property name='label' value='Merged'/&gt;\n    &lt;apps:property name='sizeOperator' value='s_sl'/&gt;\n    &lt;apps:property name='sizeUnit' value='s_smb'/&gt;\n  &lt;/entry&gt;\n  &lt;entry&gt;\n    &lt;category term='filter'&gt;&lt;/category&gt;\n    &lt;content&gt;&lt;/content&gt;\n    &lt;apps:property name='hasTheWord' value='&amp;quot;@&lt;YOUR_GITHUB_USERNAME_HERE&gt;&amp;quot;'/&gt;\n    &lt;apps:property name='label' value='Mention'/&gt;\n    &lt;apps:property name='sizeOperator' value='s_sl'/&gt;\n    &lt;apps:property name='sizeUnit' value='s_smb'/&gt;\n  &lt;/entry&gt;\n  &lt;entry&gt;\n    &lt;category term='filter'&gt;&lt;/category&gt;\n    &lt;content&gt;&lt;/content&gt;\n    &lt;apps:property name='hasTheWord' value='because you authored the thread'/&gt;\n    &lt;apps:property name='label' value='Author'/&gt;\n    &lt;apps:property name='sizeOperator' value='s_sl'/&gt;\n    &lt;apps:property name='sizeUnit' value='s_smb'/&gt;\n  &lt;/entry&gt;\n  &lt;entry&gt;\n    &lt;category term='filter'&gt;&lt;/category&gt;\n    &lt;content&gt;&lt;/content&gt;\n    &lt;apps:property name='hasTheWord' value='modified the open/close state'/&gt;\n    &lt;apps:property name='label' value='Reopened'/&gt;\n    &lt;apps:property name='sizeOperator' value='s_sl'/&gt;\n    &lt;apps:property name='sizeUnit' value='s_smb'/&gt;\n  &lt;/entry&gt;\n  &lt;entry&gt;\n    &lt;category term='filter'&gt;&lt;/category&gt;\n    &lt;content&gt;&lt;/content&gt;\n    &lt;apps:property name='hasTheWord' value='&amp;quot;requested review from @&lt;YOUR_GITHUB_TEAM_NAME_HERE&gt;&amp;quot;'/&gt;\n    &lt;apps:property name='label' value='Team review request'/&gt;\n    &lt;apps:property name='sizeOperator' value='s_sl'/&gt;\n    &lt;apps:property name='sizeUnit' value='s_smb'/&gt;\n  &lt;/entry&gt;\n  &lt;entry&gt;\n    &lt;category term='filter'&gt;&lt;/category&gt;\n    &lt;content&gt;&lt;/content&gt;\n    &lt;apps:property name='hasTheWord' value='&amp;quot;because you are on a team that was mentioned&amp;quot; AND &amp;quot;You can view, comment on, or merge this pull request online at&amp;quot;'/&gt;\n    &lt;apps:property name='label' value='Team review request'/&gt;\n    &lt;apps:property name='sizeOperator' value='s_sl'/&gt;\n    &lt;apps:property name='sizeUnit' value='s_smb'/&gt;\n  &lt;/entry&gt;\n  &lt;entry&gt;\n    &lt;category term='filter'&gt;&lt;/category&gt;\n    &lt;content&gt;&lt;/content&gt;\n    &lt;apps:property name='from' value='github'/&gt;\n    &lt;apps:property name='hasTheWord' value='&amp;quot;Closed \\#&amp;quot; &amp;quot;You are receiving this because&amp;quot;'/&gt;\n    &lt;apps:property name='doesNotHaveTheWord' value='dependabot'/&gt;\n    &lt;apps:property name='label' value='Closed'/&gt;\n    &lt;apps:property name='sizeOperator' value='s_sl'/&gt;\n    &lt;apps:property name='sizeUnit' value='s_smb'/&gt;\n  &lt;/entry&gt;\n  &lt;entry&gt;\n    &lt;category term='filter'&gt;&lt;/category&gt;\n    &lt;content&gt;&lt;/content&gt;\n    &lt;apps:property name='hasTheWord' value='&amp;quot;requested your review on&amp;quot;'/&gt;\n    &lt;apps:property name='label' value='Direct review request'/&gt;\n    &lt;apps:property name='sizeOperator' value='s_sl'/&gt;\n    &lt;apps:property name='sizeUnit' value='s_smb'/&gt;\n  &lt;/entry&gt;\n&lt;/feed&gt;\n</code></pre></div></div>\n\n<h2 id=\"2-edit-the-template-with-your-information\">2. Edit the template with your information</h2>\n\n<p>In the template, replace the following string:</p>\n\n<ul>\n  <li>\n<code class=\"language-plaintext highlighter-rouge\">@&lt;YOUR_GITHUB_USERNAME_HERE&gt;</code> (in my case that would be <code class=\"language-plaintext highlighter-rouge\">@maximevaillancourt</code>)</li>\n</ul>\n\n<p>If you’re part of a GitHub team that other people mention in issues and pull requests, also replace the following string:</p>\n\n<ul>\n  <li>\n<code class=\"language-plaintext highlighter-rouge\">@&lt;YOUR_GITHUB_TEAM_NAME_HERE&gt;</code> (for example, <code class=\"language-plaintext highlighter-rouge\">@my-organization/best-team-ever</code>)</li>\n</ul>\n\n<p>If you’re not part of a GitHub team, remove the <code class=\"language-plaintext highlighter-rouge\">&lt;entry&gt;</code> node that looks for this pattern.</p>\n\n<p>Finally, save the XML file.</p>\n\n<h2 id=\"3-import-the-xml-filters-in-gmail\">3. Import the XML filters in Gmail</h2>\n\n<p>Now that your template is ready, it’s time to import it in your Gmail account.</p>\n\n<p>Open up your Gmail settings, and click on the “Filters” tab. Once you’re there, find the “Import filters” link near the bottom of the pane. Select the XML file you just modified and upload it. Review the filters and and confirm the import.</p>\n\n<p>That’s it - you’re done. At this point, your emails should be labeled automatically. Feel free to change the colors of the labels via the “Labels” tab in your Gmail settings. I find it helpful to make <span style=\"color: #e30000;\">direct review requests</span> red as this means “urgent and important” to me, and the label for <span style=\"color: green;\">merged pull requests</span> is green because it means “success, everything’s good” to me.</p>\n\n<p>Experiment to find what works best for you.</p>\n",
            "url": "https://maximevaillancourt.com/blog/github-email-notifications-gmail-filters",
            "summary": "Cut through the noise and identify what’s important.",
            "date_published": "2020-10-15 00:00:00 +0000"
            
        },

        {
            "id": "/blog/shopify-storefront-renderer",
            "title": "How Shopify reduced storefront response times with a rewrite",
            "content_html": "<p>This post <a href=\"https://engineering.shopify.com/blogs/engineering/how-shopify-reduced-storefront-response-times-rewrite\" target=\"blank\">originally appeared on the Shopify Engineering blog</a> on August 20, 2020.</p>\n\n<p><img src=\"https://cdn.shopify.com/s/files/1/0779/4361/articles/sewing-digital-product_1000x400_crop_top.jpg\"></p>\n\n<p>In January 2019, we set out to rewrite the critical software that powers all online storefronts on Shopify’s platform to offer the fastest online shopping experience possible, entirely from scratch and without downtime.</p>\n<p>The Storefront Renderer is a server-side application that loads a Shopify merchant's storefront Liquid theme, along with the data required to serve the request (for example product data, collection data, inventory information, and images), and returns the HTML response back to your browser. Shaving milliseconds off response time leads to big results for merchants on the platform as buyers increasingly expect pages to load quickly, and failing to deliver on performance can hinder sales, not to mention other important signals like SEO.</p>\n<p>The previous storefront implementation‘s development, started over 15 years ago when Tobi launched Snowdevil, lived within Shopify’s Ruby on Rails monolith. Over the years, we realized that the “storefront” part of Shopify is quite different from the other parts of the monolith: it has much stricter performance requirements and can accept more complexity implementation-wise to improve performance, whereas other components (such as payment processing) need to favour correctness and readability.</p>\n<p>In addition to this difference in paradigm, storefront requests progressively became slower to compute as we saw more storefront traffic on the platform. This performance decline led to a direct impact on our merchant storefronts’ performance, where time-to-first-byte metrics from Shopify servers slowly crept up as time went on.</p>\n<p>Here’s how the previous architecture looked:</p>\n<p style=\"text-align: center;\"><img alt=\"Old Storefront Implementation \" src=\"//cdn.shopify.com/s/files/1/0779/4361/files/OldArchitecture.jpg?v=1597931630\"><br><em>Old Storefront Implementation </em></p>\n<p>Before, the Rails monolith handled almost all kinds of traffic: checkout, admin, APIs, and storefront.</p>\n<p>With the new implementation, traffic routing looks like this:</p>\n<p style=\"text-align: center;\"><img alt=\"New Storefront Implementation \" src=\"//cdn.shopify.com/s/files/1/0779/4361/files/NewArchitecture.jpg?v=1597931738\"><br><em>New Storefront Implementation </em></p>\n<p>The Rails monolith still handles checkout, admin, and API traffic, but storefront traffic is handled by the new implementation.</p>\n<p>Designing the new storefront implementation from the ground up allowed us to think about the guarantees we could provide: we took the opportunity of this evergreen project to set us up on strong primitives that can be extended in the future, which would have been much more difficult to retrofit in the legacy implementation. An example of these foundations is the decision to design the new implementation on top of an active-active replication setup. As a result, the new implementation always reads from dedicated read replicas, improving performance and reducing load on the primary writers.</p>\n<p>Similarly, by rebuilding and extracting the storefront-related code in a dedicated application, we took the opportunity to think about building the best developer experience possible: great debugging tools, simple onboarding setup, welcoming documentation, and so on.</p>\n<p>Finally, with improving performance as a priority, we work to increase resilience and capacity in high load scenarios (think flash sales: events where a large number of buyers suddenly start shopping on a specific online storefront), and invest in the future of storefront development at Shopify. The end result is a <strong>fast, resilient, single-purpose application</strong> that serves high-throughput online storefront traffic for merchants on the Shopify platform as quickly as possible.</p>\n<h2>Defining our success criteria</h2>\n<p>Once we clearly outlined the problem we’re trying to solve and scoped out the project, we defined three main success criteria:</p>\n<ul>\n<li>\n<strong>Establishing feature parity</strong>: for a given input, both implementations generate the same output.</li>\n<li>\n<strong>Improving performance</strong>: the new implementation runs on active-active replication setup and minimizes server response times.</li>\n<li>\n<strong>Improving resilience and capacity</strong>: in high-load scenarios, the new implementation generally sustains traffic without causing errors.</li>\n</ul>\n<h2>Building a verifier mechanism</h2>\n<p>Before building the new implementation, we needed a way to make sure that whatever we built would behave the same way as the existing implementation. So, we built a verifier mechanism that compares the output of both implementations and returns a positive or negative result depending on the outcome of the comparison.</p>\n<p>This verification mechanism runs on storefront traffic in production, and it keeps track of verification results so we can identify differences in output that need fixing. Running the verifier mechanism on production traffic (in addition to comparing the implementations locally through a formal specification and a test suite) lets us identify the most impactful areas to work on when fixing issues, and keeps us focused on the prize: reaching feature parity as quickly as possible. It’s desirable for multiple reasons:</p>\n<ul>\n<li>giving us an idea of progress and spreading the risk over a large amount of time</li>\n<li>shortening the period of time that developers at Shopify work with two concurrent implementations at once</li>\n<li>providing value to Shopify merchants as soon as possible.</li>\n</ul>\n<p>There are two parts to the entire verifier mechanism implementation:</p>\n<ol>\n<li>A verifier service (implemented in Ruby) compares the two responses we provide and returns a positive or negative result depending on the verification outcome. Similar to a `diff` tool, it lets us identify differences between the new and legacy implementations.</li>\n<li>A custom nginx routing module (implemented in Lua on top of <a href=\"https://github.com/openresty/lua-nginx-module\" target=\"blank\" title=\"OpenResty\" rel=\"noopener noreferrer\">OpenResty</a>) sends a sample of production traffic to the verifier service for verification. This module acts as a router depending on the result of the verifications for subsequent requests.</li>\n</ol>\n<p>The following diagram shows how each part interacts with the rest of the architecture:</p>\n<p style=\"text-align: center;\"><img src=\"//cdn.shopify.com/s/files/1/0779/4361/files/VerifierMechansim_1A.jpg?v=1597932504\" alt=\"Legacy implementation and new implementation at the same conceptual layer\"><br><em>Legacy implementation and new implementation at the same conceptual layer</em></p>\n<p>The legacy implementation (the Rails monolith) still exists, and the new implementation (including the Verifier service) is introduced at the same conceptual layer. Both implementations are placed behind a custom routing module that decides where to route traffic based on the request attributes and the verification data for this request type. Let’s look at an example.</p>\n<p>When a buyer’s device sends an initial request for a given storefront page (for example, a product page from shop XYZ), the request is sent to Shopify’s infrastructure, at which point an nginx instance handles it. The routing module considers the request attributes to determine if other shop XYZ product page requests have previously passed verification.</p>\n<p style=\"text-align: center;\"><img alt=\"First request routed to Legacy implementation\" src=\"//cdn.shopify.com/s/files/1/0779/4361/files/VerifierMechansim_1.jpg?v=1597932239\"><br><em>First request routed to Legacy implementation</em></p>\n<p>Since this is the first request of this kind in our example, the routing module sends the request to the legacy implementation to get a baseline reference that it will use for subsequent shop XYZ product page requests.</p>\n<p style=\"text-align: center;\"><img alt=\"Routing module sends original request and legacy implementation’s response to the new implementation\" src=\"//cdn.shopify.com/s/files/1/0779/4361/files/VerifierMechansim_2.jpg?v=1597932398\"><br><em>Routing module sends original request and legacy implementation’s response to the new implementation</em></p>\n<p>Once the response comes back from the legacy implementation, the Lua routing module sends that response to the buyer. In the background, the Lua routing module also sends both the original request and the legacy implementation’s response to the new implementation. The new implementation computes a response to the original request and feeds both its response and the forwarded legacy implementation’s response to the verifier service. This is done asynchronously to make sure we’re not adding latency to responses we send to buyers, who don’t notice anything different.</p>\n<p>At this point, the verifier service received the responses from both the legacy and new implementations and is ready to compare them. Of course, the legacy implementation is assumed to be correct as it’s been running in production for years now (it acts as our reference point). We keep track of differences between the two implementations’ responses so we can debug and fix them later. The verifier service looks at both responses’ status code, headers, and body, ensuring they’re equivalent. This lets us identify any differences in the responses so we make sure our new implementation behaves like the legacy one.</p>\n<p>Time-related and randomness-related exceptions make it impossible to have exactly byte-equal responses, so we ignore certain patterns in the verifier service to relax the equivalence criteria. The verifier service uses a fixed time value during the comparison process and sets any random values to a known value so we reliably compare the outputs containing time-based and randomness-based differences.</p>\n<p style=\"text-align: center;\"><img alt=\"The verifier service sends comparison result back to the Lua module\" src=\"//cdn.shopify.com/s/files/1/0779/4361/files/VerifierMechansim_3.jpg?v=1597933710\"><br><em>The verifier service sends comparison result back to the Lua module</em></p>\n<p>The verifier service sends the outcome of the comparison back to the Lua module, which keeps track of that comparison outcome for subsequent requests of the same kind.</p>\n<h2>Dynamically routing requests to the new implementation</h2>\n<p>Once we had verified our new approach, we tested rendering a page using the new implementation instead of the legacy one. We iterated upon our verification mechanism to allow us to route traffic to the new implementation after a given number of successful verifications. Here’s how it works.</p>\n<p>Just like when we only verified traffic, a request arrives from a client device and hits Shopify’s architecture. The request is sent to both implementations, and both outputs are forwarded to the verifier service for comparison. The comparison result is sent back to the Lua routing module, which keeps track of it for future requests.</p>\n<p>When a subsequent storefront request arrives from a buyer and reaches the Lua routing module, it decides where to send it based on the previous verification results for requests similar to the current one (based on the request attributes</p>\n<p style=\"text-align: center;\"><img alt=\"For subsequent storefront requests, the Lua routing module decides where to send it\" src=\"//cdn.shopify.com/s/files/1/0779/4361/files/VerifierMechansim_4.jpg?v=1597933966\"><br><em>For subsequent storefront requests, the Lua routing module decides where to send it</em></p>\n<p>If the request was verified multiple times in the past, and nearly all outcomes from the verifier service were “Pass”, then we consider the request safe to be served by the new implementation.</p>\n<p style=\"text-align: center;\"><img alt=\"If nearly all verifier service results are “Pass”, then it uses the new implementation\" src=\"//cdn.shopify.com/s/files/1/0779/4361/files/VerifierMechansim_5.jpg?v=1597934257\"><br><em>If most verifier service results are “Pass”, then it uses the new implementation</em></p>\n<p>If, on the other hand, some verifications failed for this kind of request, we’ll play it safe and send the request to the legacy implementation.</p>\n<p style=\"text-align: center;\"><img alt=\"If most verifier service results are “Fail”, then it uses the old implementation\" src=\"//cdn.shopify.com/s/files/1/0779/4361/files/VerifierMechansim_6.jpg?v=1597934545\"><br><em>If most verifier service results are “Fail”, then it uses the old implementation</em></p>\n<h2>Successfully Rendering In Production</h2>\n<p>With the verifier mechanism and the dynamic router in place, our first goal was to render one of the simplest storefront pages that exists on the Shopify platform: the password page that protects a storefront before the merchant makes it available to the public.</p>\n<p>Once we reached full parity for a single shop’s password page, we tested our implementation in production (for the first time) by routing traffic for this password page to the new implementation for a couple of minutes to test it out.</p>\n<p>Success! The new implementation worked in production. It was time to start implementing everything else.</p>\n<h2>Increasing feature parity</h2>\n<p>After our success with the password page, we tackled the most frequently accessed storefront pages on the platform (product pages, collection pages, etc). Diff by diff, endpoint by endpoint, we slowly increased the parity rate between the legacy and new implementations.</p>\n<p>Having both implementations running at the same time gave us a safety net to work with so that if we introduced a regression, requests would easily be routed to the legacy implementation instead. Conversely, whenever we shipped a change to the new implementation that would fix a gap in feature parity, the verifier service starts to report verification successes, and our custom routing module in nginx automatically starts sending traffic to the new implementation after a predetermined time threshold.</p>\n<h2>Defining “good” performance with Apdex scores</h2>\n<p>We collected <a href=\"https://docs.newrelic.com/docs/apm/new-relic-apm/apdex/apdex-measure-user-satisfaction#:~:text=Apdex%20score,as%20half%20a%20satisfied%20request\" target=\"blank\" title=\"Apdex: Measure user satisfaction\" rel=\"noopener noreferrer\">Apdex</a> (Application Performance Index) scores on server-side processing time for both the new and legacy implementations to compare them.</p>\n<p>To calculate Apdex scores, we defined a parameter for a satisfactory threshold response time (this is the Apdex’s “T” parameter). Our threshold response time to define a frustrating experience would then be “above 4T” (defined by Apdex).</p>\n<p>We defined our “T” parameter as 200ms, which lines up with Google’s PageSpeed Insights recommendation for server response times. We consider server processing time below 200ms as satisfying and a server processing time of 800ms or more as frustrating. Anything in between is tolerated.</p>\n<p>From there, calculating the Apdex score for a given implementation consists of setting a time frame, and counting three values:</p>\n<ul>\n<li>n, the total number of responses in the defined time frame</li>\n<li>s, the number of satisfying responses (faster than 200ms) in the time frame</li>\n<li>t, the number of tolerated responses (between 200ms and 800ms) in the time frame</li>\n</ul>\n<p>Then, we calculate the Apdex score: </p>\n<meta charset=\"utf-8\">\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>apdexScore = (s + t/2) / n\n</code></pre></div></div>\n\n<p>By calculating Apdex scores for both the legacy and new implementations using the same t parameter, we had common ground to compare their performance.</p>\n<h2>Methods to improve server-side storefront performance</h2>\n<p>We want all Shopify storefronts to be fast, and this new implementation aims to speed up what a performance-conscious theme developer can’t by optimizing data access patterns, reducing memory allocations, and implementing efficient caching layers.</p>\n<h3>Optimizing data access patterns</h3>\n<p>The new implementation uses optimized, handcrafted SQL multi-select statements maximizing the amount of data transferred in a single round trip. We carefully vet what we eager-load depending on the type of request and we optimize towards reducing instances of N+1 queries.</p>\n<h3>Reducing memory allocations</h3>\n<p>We reduce the number of memory allocations as much as possible so Ruby spends less time in garbage collection. We use methods that apply modifications in place (such as #map!) rather than those that allocate more memory space (like #map). This kind of performance-oriented Ruby paradigm sometimes leads to code that’s not as simple as idiomatic Ruby, but paired with proper testing and verification, this tradeoff provides big performance gains. It may not seem like much, but those memory allocations add up quickly, and considering the amount of storefront traffic Shopify handles, every optimization counts.</p>\n<h3>Implementing efficient caching layers</h3>\n<p>We implemented various layers of caching throughout the application to reduce expensive calls. Frequent database queries are partitioned and cached to optimize for subsequent reads in a key-value store, and in the case of extremely frequent queries, those are cached directly in application memory to reduce I/O latency. Finally, the results of full page renders are cached too, so we can simply serve a full HTTP response directly from cache if possible.</p>\n<h2>Measuring performance improvement successes</h2>\n<p>Once we could measure the performance of both implementations and reach a high enough level of verified feature parity, we started migrating merchant shops. Here are some of the improvements we’re seeing with our new implementation:</p>\n<ul>\n<li>Across all shops, average server response times for requests served by the new implementation are 4x to 6x faster than the legacy implementation. This is huge!</li>\n<li>When migrating a storefront to the new implementation, we see that the Apdex score for server-side processing time improves by +0.11 on average.</li>\n<li>When only considering cache misses (requests that can’t be served directly from the cache and need to be computed from scratch), the new implementation increases the Apdex score for server-side processing time by a full +0.20 on average compared to the previous implementation.</li>\n<li>We heard back from merchants mentioning a 500ms improvement in time-to-first-byte metrics when the new implementation was rolled out to their storefront.</li>\n</ul>\n<p>So another success! We improved store performance in production.</p>\n<p>Now how do we make sure this translates to our third success criteria?</p>\n<h2>Improving sesilience and capacity</h2>\n<p>While working on the new implementation, the Verifier service identified potential parity gaps, which helped tremendously. However, a few times we shipped code to production that broke in exceedingly rare edge cases that it couldn’t catch.</p>\n<p>As a safety mechanism, we made it so that whenever the new implementation would fail to successfully render a given request, we’d fall back to the legacy implementation. The response would be slower, but at least it was working properly. We used circuit breakers in our custom nginx routing module so that we’d open the circuit and start sending traffic to the legacy implementation if the new implementation was having trouble responding successfully. Read more on <a href=\"https://engineering.shopify.com/blogs/engineering/circuit-breaker-misconfigured\" target=\"blank\" title=\"Your Circuit Breaker is Misconfigured\" rel=\"noopener noreferrer\">tuning circuit breakers in this blog post by my teammate Damian Polan</a>.</p>\n<h3>Increase capacity in high-load scenarios</h3>\n<p>To ensure that the new implementation responds well to flash sales, we implemented and tweaked two mechanisms. The first one is an automatic scaling mechanism that adds or remove computing capacity in response to the amount of load on the current swarm of computers that serve traffic. If load increases as a result of an increase in traffic, the autoscaler will detect this increase and start provisioning more compute capacity to handle it.</p>\n<p>Additionally, we introduced in-memory cache to reduce load on external data stores for storefronts that put a lot of pressure on the platform’s resources. This provides a buffer that reduces load on very-high traffic shops.</p>\n<h3>Failing fast</h3>\n<p>When an external data store isn’t available, we don’t want to serve buyers an error page. If possible, we’ll try to gracefully fall back to a safe way to serve the request. It may not be as fast, or as complete as a normal, healthy response, but it’s definitely better than serving a sad error page.</p>\n<p>We implemented circuit breakers on external datastores using <a href=\"https://github.com/Shopify/semian\" target=\"blank\" title=\"Semian is a library for controlling access to slow or unresponsive external services to avoid cascading failures.\" rel=\"noopener noreferrer\">Semian</a>, a Shopify-developed Ruby gem that controls access to slow or unresponsive external services, avoiding cascading failures and making the new implementation more resilient to failure.</p>\n<p>Similarly, if a cache store isn’t available, we’ll quickly consider the timeout as a cache miss, so instead of failing the entire request because the cache store wasn’t available, we’ll simply fetch the data from the canonical data store instead. It may take longer, but at least there’s a successful response to serve back to the buyer.</p>\n<h3>Testing failure scenarios and the limits of the new implementation</h3>\n<p>Finally, as a way to identify potential resilience issues, the new implementation uses <a href=\"https://github.com/Shopify/toxiproxy\" target=\"blank\" title=\"Toxiproxy - A TCP proxy to simulate network and system conditions for chaos and resiliency testing\" rel=\"noopener noreferrer\">Toxiproxy</a> to generate test cases where various resources are made available or not, on demand, to generate problematic scenarios.</p>\n<p>As we put these resilience and capacity mechanisms in place, we regularly ran load tests using internal tooling to see how the new implementation behaves in the face of a large amount of traffic. As time went on, we increased the new implementation’s resilience and capacity significantly, removing errors and exceptions almost completely even in high-load scenarios. With BFCM 2020 coming soon (which we consider as an organic, large-scale load test), we’re excited to see how the new implementation behaves.</p>\n<h2>Where we’re at currently</h2>\n<p>We’re currently in the process of rolling out the new implementation to all online storefronts on the platform. This process happens automatically, without the need for any intervention from Shopify merchants. While we do this, we’re adding more features to the new implementation to bring it to full parity with the legacy implementation. The new implementation is currently at 90%+ feature parity with the legacy one, and we’re increasing that figure every day with the goal of reaching 100% parity to retire the legacy implementation.</p>\n<p>As we roll out the new implementation to storefronts we are continuing to see and measure performance improvements as well. On average, server response times for the new implementation are 4x faster than the legacy implementation. Rhone Apparel, a Shopify Plus merchant, started using the new implementation in April 2020 and <a href=\"https://www.shopify.com/enterprise/site-performance-page-speed-ecommerce#fast-ecommerce-platform-1\" target=\"blank\" title=\"Improve Your Ecommerce Site Performance &amp; Speed to 2X Conversions\" rel=\"noopener noreferrer\">saw dramatic improvements in server-side performance over the previous month</a>.</p>\n<p>We learned a lot during the process of rewriting this critical piece of software. The strong foundations of this new implementation make it possible to deploy it around the world, closer to buyers everywhere, to reduce network latency involved in cross-continental networking, and we continue to explore ways to make it even faster while providing the best developer experience possible to set us up for the future.</p>\n\n<p><em>This blog post is <strong>not</strong> available under a Creative Commons license.</em></p>\n",
            "url": "https://maximevaillancourt.com/blog/shopify-storefront-renderer",
            "summary": "Making server response times 4x faster than before.",
            "date_published": "2020-09-03 00:00:00 +0000"
            
        },

        {
            "id": "/blog/debugging-ruby-casecmp-bug-using-gdb",
            "title": "Two Ruby apps, same code, different output",
            "content_html": "<p>I noticed something odd today while working on two different Ruby codebases. This simple line of Ruby behaved differently in both applications:</p>\n\n<div class=\"language-rb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"s2\">\"luck\"</span><span class=\"p\">.</span><span class=\"nf\">casecmp</span><span class=\"p\">(</span><span class=\"s2\">\"L`auguste\"</span><span class=\"p\">)</span>\n</code></pre></div></div>\n\n<p>Executing <code class=\"language-plaintext highlighter-rouge\">\"luck\".casecmp(\"L`auguste\")</code> in application A returned <code class=\"language-plaintext highlighter-rouge\">-1</code>, while executing it in application B returned <code class=\"language-plaintext highlighter-rouge\">1</code>.</p>\n\n<p>“Did the alphabet change at some point and I didn’t get the memo?”, I thought.</p>\n\n<blockquote>\n  <p><strong>Aside</strong></p>\n\n  <p><a href=\"https://ruby-doc.org/core-2.7.1/String.html#method-i-casecmp\" target=\"blank\"><code class=\"language-plaintext highlighter-rouge\">String#casecmp</code></a> is a built-in Ruby method that returns <code class=\"language-plaintext highlighter-rouge\">-1</code>, <code class=\"language-plaintext highlighter-rouge\">0</code>, <code class=\"language-plaintext highlighter-rouge\">1</code>, or <code class=\"language-plaintext highlighter-rouge\">nil</code> depending on whether the object on which it’s called is less than, equal to, or greater than the function argument, and it does so in case-insensitive fashion. Here are a few simple examples of how it behaves:</p>\n\n  <div class=\"language-rb highlighter-rouge\">\n<div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"s2\">\"aBcDeF\"</span><span class=\"p\">.</span><span class=\"nf\">casecmp</span><span class=\"p\">(</span><span class=\"s2\">\"abcde\"</span><span class=\"p\">)</span>     <span class=\"c1\">#=&gt; 1</span>\n<span class=\"s2\">\"aBcDeF\"</span><span class=\"p\">.</span><span class=\"nf\">casecmp</span><span class=\"p\">(</span><span class=\"s2\">\"abcdef\"</span><span class=\"p\">)</span>    <span class=\"c1\">#=&gt; 0</span>\n<span class=\"s2\">\"aBcDeF\"</span><span class=\"p\">.</span><span class=\"nf\">casecmp</span><span class=\"p\">(</span><span class=\"s2\">\"abcdefg\"</span><span class=\"p\">)</span>   <span class=\"c1\">#=&gt; -1</span>\n<span class=\"s2\">\"abcdef\"</span><span class=\"p\">.</span><span class=\"nf\">casecmp</span><span class=\"p\">(</span><span class=\"s2\">\"ABCDEF\"</span><span class=\"p\">)</span>    <span class=\"c1\">#=&gt; 0</span>\n</code></pre></div>  </div>\n</blockquote>\n\n<h2 id=\"looking-for-monkey-patches\">Looking for monkey patches</h2>\n\n<p>Seeing as one of the applications is built on top of Ruby of Rails and the other isn’t, my first thought was that maybe there was a Rails and/or ActiveSupport patch on <code class=\"language-plaintext highlighter-rouge\">String#casecmp</code> that would change the behavior of this line in one of the applications. However, I didn’t find anything that pointed to this. I kept digging, hoping to maybe find a patch in the <em>other</em> application that could explain this difference in behavior. Again, I didn’t find anything. 🙈</p>\n\n<h2 id=\"different-rubies\">Different Rubies</h2>\n\n<p>Eventually, after exploring a bit more, I realized that both applications ran on different versions of Ruby: application A was on Ruby 2.6, while application B was using Ruby 2.7.</p>\n\n<p>Running the same command on both versions of Ruby indeed gives us different results:</p>\n\n<div class=\"language-sh highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ </span>~/.rubies/ruby-2.6.6/bin/ruby <span class=\"nt\">-e</span> <span class=\"s1\">'puts \"luck\".casecmp(\"L`Auguste\")'</span>\n<span class=\"nt\">-1</span>\n\n<span class=\"nv\">$ </span>~/.rubies/ruby-2.7.0/bin/ruby <span class=\"nt\">-e</span> <span class=\"s1\">'puts \"luck\".casecmp(\"L`Auguste\")'</span>\n1\n</code></pre></div></div>\n\n<p>Ah ha! We’re getting closer. While I could have called it a day here and simply updated application B to Ruby 2.7 to resolve the issue, I wanted to understand: what causes it?</p>\n\n<h2 id=\"changelogs--bindingpry\">Changelogs &amp; <code class=\"language-plaintext highlighter-rouge\">binding.pry</code>\n</h2>\n\n<p>I then started to comb through Ruby changelogs, trying to find if anything changed between Ruby 2.6 and Ruby 2.7 for <code class=\"language-plaintext highlighter-rouge\">String#casecmp</code>, or anything somehow related to string comparison. I didn’t find anything.</p>\n\n<p>Of course, it would be nice to debug this using <code class=\"language-plaintext highlighter-rouge\">binding.pry</code> or other similar Ruby-level debugging tools by stepping into the <code class=\"language-plaintext highlighter-rouge\">String#casecmp</code> call to see what’s going on inside. However, this doesn’t get us very far, as trying to use Ruby’s <code class=\"language-plaintext highlighter-rouge\">Tracer</code> or <code class=\"language-plaintext highlighter-rouge\">binding.pry</code> doesn’t really help.</p>\n\n<p>Running this:</p>\n\n<div class=\"language-sh highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ </span>ruby <span class=\"nt\">-r</span> tracer <span class=\"nt\">-e</span> <span class=\"s1\">'\"luck\".casecmp(\"L`Auguste\")'</span>\n</code></pre></div></div>\n\n<p>returns this output:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>#0:-e:1::-: \"luck\".casecmp(\"L`Auguste\")\n</code></pre></div></div>\n\n<p>and not much else. That’s because <code class=\"language-plaintext highlighter-rouge\">String#casecmp</code> is implemented in C, directly inside MRI’s <a href=\"https://github.com/ruby/ruby/blob/master/string.c\" target=\"blank\"><code class=\"language-plaintext highlighter-rouge\">string.c</code></a>, so there’s no actual Ruby code underneath <code class=\"language-plaintext highlighter-rouge\">String#casecmp</code> that we can step into using Ruby-level debugging tools.</p>\n\n<p>Here comes the GDB part: because we’re essentially dealing with C code at this point, we can use GDB to understand what happens inside the call to <code class=\"language-plaintext highlighter-rouge\">String#casecmp</code>. So with that, I fired up GDB for the first time in years (I typically work with Ruby, so GDB is not something I commonly use).</p>\n\n<h2 id=\"identifying-the-root-cause-using-gdb\">Identifying the root cause using GDB</h2>\n\n<p>Let’s see how to use GDB to understand why both Ruby 2.6 and Ruby 2.7 behave differently with the same input to <code class=\"language-plaintext highlighter-rouge\">String#casecmp</code>.</p>\n\n<p>I first prepared a simple Ruby file containing the source that replicates the issue:</p>\n\n<div class=\"language-rb highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"c1\"># ~/casecmp.rb</span>\n<span class=\"nb\">puts</span> <span class=\"s2\">\"luck\"</span><span class=\"p\">.</span><span class=\"nf\">casecmp</span><span class=\"p\">(</span><span class=\"s2\">\"L`Auguste\"</span><span class=\"p\">)</span>\n</code></pre></div></div>\n\n<p>Notice that the second character in the input to <code class=\"language-plaintext highlighter-rouge\">casecmp</code> is a backtick (<code class=\"language-plaintext highlighter-rouge\">`</code>), which has ASCII code 96. This is relevant for paragraphs below.</p>\n\n<h3 id=\"in-ruby-270\">In Ruby 2.7.0</h3>\n\n<p>Let’s start by firing up GDB with a self-compiled version of Ruby 2.7.0:</p>\n\n<div class=\"language-sh highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ </span><span class=\"nb\">sudo </span>gdb /Users/maximevaillancourt/.rubies/ruby-2.7.0/bin/ruby\nReading symbols from /Users/maximevaillancourt/.rubies/ruby-2.7.0/bin/ruby...\n</code></pre></div></div>\n\n<p>Then, we add a breakpoint on the <code class=\"language-plaintext highlighter-rouge\">str_casecmp</code> function so execution pauses once we reach it:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>(gdb) break str_casecmp\nBreakpoint 1 at 0x1001fa766: file string.c, line 3371.\n</code></pre></div></div>\n\n<p>Perfect. We’re now ready to run the <code class=\"language-plaintext highlighter-rouge\">casecmp.rb</code> Ruby script from above.</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>(gdb) run casecmp.rb\nStarting program: /Users/maximevaillancourt/.rubies/ruby-2.7.0/bin/ruby casecmp.rb\n</code></pre></div></div>\n\n<p>We eventually hit the breakpoint we just set:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>Thread 2 hit Breakpoint 1, str_casecmp (str1=4329352680, str2=4329352640) at string.c:3371\n3371\t    enc = rb_enc_compatible(str1, str2);\n</code></pre></div></div>\n\n<blockquote>\n  <p><strong>Aside</strong></p>\n\n  <p>Internally, <a href=\"https://github.com/ruby/ruby/blob/4318aba9c94ebff53e4168886e1a35a24013924f/string.c#L3467-L3468\" target=\"blank\"><code class=\"language-plaintext highlighter-rouge\">String#str_casecmp</code></a> is quite simple: it iterates over each character in both inputs by index starting from the first character, converting both characters to the same case so that the function behaves in a case-insensitive way, and returns early if the two currently considered characters from each input are different. In doing so, it determines which character is “bigger” than the other using the character code (an <a href=\"http://www.asciitable.com/\" target=\"blank\">ASCII code table</a> is a useful asset to have nearby for the rest of this blog post).</p>\n</blockquote>\n\n<p>In Ruby 2.7.0, notice that the case conversion <a href=\"https://github.com/ruby/ruby/blob/e9e4f8430a62f56a4e62dd728f4498ee4c300c12/string.c#L3381-L3382\" target=\"blank\">converts both inputs to lowercase using <code class=\"language-plaintext highlighter-rouge\">TOLOWER</code></a>:</p>\n\n<div class=\"language-c highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">while</span> <span class=\"p\">(</span><span class=\"n\">p1</span> <span class=\"o\">&lt;</span> <span class=\"n\">p1end</span> <span class=\"o\">&amp;&amp;</span> <span class=\"n\">p2</span> <span class=\"o\">&lt;</span> <span class=\"n\">p2end</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n  <span class=\"k\">if</span> <span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">p1</span> <span class=\"o\">!=</span> <span class=\"o\">*</span><span class=\"n\">p2</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n    <span class=\"kt\">unsigned</span> <span class=\"kt\">int</span> <span class=\"n\">c1</span> <span class=\"o\">=</span> <span class=\"n\">TOLOWER</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">p1</span> <span class=\"o\">&amp;</span> <span class=\"mh\">0xff</span><span class=\"p\">);</span>\n    <span class=\"kt\">unsigned</span> <span class=\"kt\">int</span> <span class=\"n\">c2</span> <span class=\"o\">=</span> <span class=\"n\">TOLOWER</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">p2</span> <span class=\"o\">&amp;</span> <span class=\"mh\">0xff</span><span class=\"p\">);</span>\n    <span class=\"k\">if</span> <span class=\"p\">(</span><span class=\"n\">c1</span> <span class=\"o\">!=</span> <span class=\"n\">c2</span><span class=\"p\">)</span>\n      <span class=\"k\">return</span> <span class=\"n\">INT2FIX</span><span class=\"p\">(</span><span class=\"n\">c1</span> <span class=\"o\">&lt;</span> <span class=\"n\">c2</span> <span class=\"o\">?</span> <span class=\"o\">-</span><span class=\"mi\">1</span> <span class=\"o\">:</span> <span class=\"mi\">1</span><span class=\"p\">);</span>\n  <span class=\"p\">}</span>\n  <span class=\"n\">p1</span><span class=\"o\">++</span><span class=\"p\">;</span>\n  <span class=\"n\">p2</span><span class=\"o\">++</span><span class=\"p\">;</span>\n<span class=\"p\">}</span>\n</code></pre></div></div>\n\n<p>After navigating in <code class=\"language-plaintext highlighter-rouge\">str_casecmp</code> using <code class=\"language-plaintext highlighter-rouge\">next</code> a few times, we enter the loop and arrive at a point where we can print <code class=\"language-plaintext highlighter-rouge\">c1</code> and <code class=\"language-plaintext highlighter-rouge\">c2</code>, which are the codes for the characters at the current index for both inputs:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>3382\t                unsigned int c2 = TOLOWER(*p2 &amp; 0xff);\n(gdb) print c1\n$11 = 108\n(gdb) next\n3383\t                if (c1 != c2)\n(gdb) print c2\n$12 = 108\n</code></pre></div></div>\n\n<p>Here’s a visual representation of the buffers:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>c1\n ↓\n108  ?   ?   ?\n l   u   c   k\n\n108  ?   ?   ?   ?   ?   ?   ?   ?\n l   `   a   u   g   u   s   t   e\n ↑\nc2\n</code></pre></div></div>\n\n<p>108 is the decimal ASCII character code representation for the first letter of both inputs: <code class=\"language-plaintext highlighter-rouge\">l</code> (lowercase “L”), so the loop continues to the next iteration because <code class=\"language-plaintext highlighter-rouge\">c1</code> and <code class=\"language-plaintext highlighter-rouge\">c2</code> are the same.</p>\n\n<p>On the second iteration of the loop (on the second character of both inputs), we get the following results:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>3382\t                unsigned int c2 = TOLOWER(*p2 &amp; 0xff);\n(gdb) print c1\n$14 = 117\n(gdb) next\n3383\t                if (c1 != c2)\n(gdb) print c2\n$16 = 96\n</code></pre></div></div>\n\n<p>Here’s a visual representation of the buffers:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>    c1\n     ↓\n108 117  ?   ?\n l   u   c   k\n\n108  96  ?   ?   ?   ?   ?   ?   ?\n l   `   a   u   g   u   s   t   e\n     ↑\n     c2\n</code></pre></div></div>\n\n<p><code class=\"language-plaintext highlighter-rouge\">c1</code> contains <code class=\"language-plaintext highlighter-rouge\">117</code>, which is the decimal ASCII character code representation for <code class=\"language-plaintext highlighter-rouge\">u</code>, while <code class=\"language-plaintext highlighter-rouge\">96</code> (in <code class=\"language-plaintext highlighter-rouge\">c2</code>) is the character code for a backtick (<code class=\"language-plaintext highlighter-rouge\">`</code>). We then enter the <code class=\"language-plaintext highlighter-rouge\">if (c1 != c2)</code> conditional, and the return value is <code class=\"language-plaintext highlighter-rouge\">1</code> because <code class=\"language-plaintext highlighter-rouge\">c1 &gt; c2</code> (<code class=\"language-plaintext highlighter-rouge\">117 &gt; 96</code>).</p>\n\n<p>Okay. So far so good. This lines up with the initial observation of the issue. How are things different in Ruby 2.6.6?</p>\n\n<h3 id=\"in-ruby-266\">In Ruby 2.6.6</h3>\n\n<p>We do almost the same setup as above (same one-line Ruby script to replicate the issue, same breakpoint on <code class=\"language-plaintext highlighter-rouge\">str_casecmp</code>), but we fire up GDB with Ruby 2.6.6:</p>\n\n<div class=\"language-sh highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"nv\">$ </span><span class=\"nb\">sudo </span>gdb /Users/maximevaillancourt/.rubies/ruby-2.6.6/bin/ruby\nReading symbols from /Users/maximevaillancourt/.rubies/ruby-2.6.6/bin/ruby...\n\n<span class=\"o\">(</span>gdb<span class=\"o\">)</span> <span class=\"nb\">break </span>str_casecmp\n...\n\n<span class=\"o\">(</span>gdb<span class=\"o\">)</span> run casecmp.rb\nStarting program: /Users/maximevaillancourt/.rubies/ruby-2.6.6/bin/ruby casecmp.rb\n\nThread 2 hit Breakpoint 1, str_casecmp ...\n</code></pre></div></div>\n\n<p>Let’s look at the loop we presented above in Ruby 2.7.0, but <a href=\"https://github.com/ruby/ruby/blob/a9a48e6a741f048766a2a287592098c4f6c7b7c7/string.c#L3413-L3414\" target=\"blank\">in Ruby 2.6.6</a> this time:</p>\n\n<div class=\"language-c highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code><span class=\"k\">while</span> <span class=\"p\">(</span><span class=\"n\">p1</span> <span class=\"o\">&lt;</span> <span class=\"n\">p1end</span> <span class=\"o\">&amp;&amp;</span> <span class=\"n\">p2</span> <span class=\"o\">&lt;</span> <span class=\"n\">p2end</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n  <span class=\"k\">if</span> <span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">p1</span> <span class=\"o\">!=</span> <span class=\"o\">*</span><span class=\"n\">p2</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n    <span class=\"kt\">unsigned</span> <span class=\"kt\">int</span> <span class=\"n\">c1</span> <span class=\"o\">=</span> <span class=\"n\">TOUPPER</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">p1</span> <span class=\"o\">&amp;</span> <span class=\"mh\">0xff</span><span class=\"p\">);</span>\n    <span class=\"kt\">unsigned</span> <span class=\"kt\">int</span> <span class=\"n\">c2</span> <span class=\"o\">=</span> <span class=\"n\">TOUPPER</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"n\">p2</span> <span class=\"o\">&amp;</span> <span class=\"mh\">0xff</span><span class=\"p\">);</span>\n    <span class=\"k\">if</span> <span class=\"p\">(</span><span class=\"n\">c1</span> <span class=\"o\">!=</span> <span class=\"n\">c2</span><span class=\"p\">)</span>\n      <span class=\"k\">return</span> <span class=\"n\">INT2FIX</span><span class=\"p\">(</span><span class=\"n\">c1</span> <span class=\"o\">&lt;</span> <span class=\"n\">c2</span> <span class=\"o\">?</span> <span class=\"o\">-</span><span class=\"mi\">1</span> <span class=\"o\">:</span> <span class=\"mi\">1</span><span class=\"p\">);</span>\n  <span class=\"p\">}</span>\n  <span class=\"n\">p1</span><span class=\"o\">++</span><span class=\"p\">;</span>\n  <span class=\"n\">p2</span><span class=\"o\">++</span><span class=\"p\">;</span>\n<span class=\"p\">}</span>\n</code></pre></div></div>\n\n<p>Notice that instead of using <code class=\"language-plaintext highlighter-rouge\">TOLOWER</code> as in Ruby 2.7.0, Ruby 2.6.6 uses <code class=\"language-plaintext highlighter-rouge\">TOUPPER</code>. Interesting.</p>\n\n<p>Let’s fast-forward to the part where we get to <code class=\"language-plaintext highlighter-rouge\">c1</code> and <code class=\"language-plaintext highlighter-rouge\">c2</code> for the second character in the input:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>3414\t\t\tunsigned int c2 = TOUPPER(*p2 &amp; 0xff);\n(gdb) next\n3415\t                if (c1 != c2)\n(gdb) print c1\n$5 = 85\n(gdb) print c2\n$6 = 96\n</code></pre></div></div>\n\n<p>Here’s a visual representation of the buffers:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>     c1\n     ↓\n108  85  ?   ?\n L   U   C   K\n\n108  96  ?   ?   ?   ?   ?   ?   ?\n L   `   A   U   G   U   S   T   E\n     ↑\n     c2\n</code></pre></div></div>\n\n<p><code class=\"language-plaintext highlighter-rouge\">c1</code> is <code class=\"language-plaintext highlighter-rouge\">85</code>, which is the character code for <code class=\"language-plaintext highlighter-rouge\">U</code>, and <code class=\"language-plaintext highlighter-rouge\">c2</code> is <code class=\"language-plaintext highlighter-rouge\">96</code> (just like in Ruby 2.7.0), which is the character code for a backtick (<code class=\"language-plaintext highlighter-rouge\">`</code>).</p>\n\n<p>This time though, the comparison result is different, because <code class=\"language-plaintext highlighter-rouge\">c1 &lt; c2</code> (<code class=\"language-plaintext highlighter-rouge\">85 &lt; 96</code>), so <code class=\"language-plaintext highlighter-rouge\">str_casecmp</code> returns <code class=\"language-plaintext highlighter-rouge\">-1</code>.</p>\n\n<p>There it is: because Ruby 2.6 uses <code class=\"language-plaintext highlighter-rouge\">TOUPPER</code> and Ruby 2.7 uses <code class=\"language-plaintext highlighter-rouge\">TOLOWER</code> before comparing the inputs, and because one of the characters to compare is a backtick (<code class=\"language-plaintext highlighter-rouge\">`</code>, which can’t be converted to uppercase or lowercase in any way), the other character’s code “moves” differently around the “fixed” backtick character code, affecting the result of the <code class=\"language-plaintext highlighter-rouge\">String#casecmp</code> function.</p>\n\n<hr>\n\n<p>To summarize, the root cause of the issue is that <code class=\"language-plaintext highlighter-rouge\">String#casecmp</code> was updated in Ruby 2.7 to <strong>lowercase</strong> the two inputs before comparing them, while Ruby 2.6 used to <strong>uppercase</strong> the two inputs before comparing them. <a href=\"https://github.com/ruby/ruby/commit/082424ef58116db9663a754157d6c441d60fd101#diff-7a2f2c7dfe0bf61d38272aeaf68ac768\" target=\"blank\">This is the commit where this change was introduced.</a></p>\n\n<p>Fun debugging session. :)</p>\n",
            "url": "https://maximevaillancourt.com/blog/debugging-ruby-casecmp-bug-using-gdb",
            "summary": "Debugging a weird Ruby string sorting issue with GDB.",
            "date_published": "2020-08-13 00:00:00 +0000"
            
        },

        {
            "id": "/blog/code-walkthroughs",
            "title": "Accelerating software onboarding with code walkthroughs",
            "content_html": "<p><img src=\"https://images.unsplash.com/photo-1573166613605-3b4dfcbf1268?ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1498&amp;h=500&amp;q=80\"></p>\n\n<p><em>By the end of this article, you’ll have a new tool in your toolbox to help newcomers quickly discover how your software project works, while reducing the time you explicitly spend introducing new folks to your project.</em></p>\n\n<hr>\n\n<p><strong>Ramping up on a new software project is hard</strong>, both for the person ramping up and for the team that supports this person’s onboarding process. It’s especially worse when multiple people start onboarding the same project at the same time, which happened to my team recently: our project’s area of influence has grown quickly recently, and as a result, we’re seeing dozens of developers starting to contribute to the project.</p>\n\n<p>This sudden spike in interest lead to what I call “onboarding load” for our core team, as we suddenly faced pressure to help newcomers learn and navigate the codebase, while simulteanously trying to keep up with our day-to-day tasks.</p>\n\n<h3 id=\"deflecting-pressure\">Deflecting pressure</h3>\n\n<p>To reduce this pressure on our core team, I recently introduced a “code walkthrough” in the project’s codebase. It’s essentially a collection of high-quality, high-context code comments “tagged” with a <code class=\"language-plaintext highlighter-rouge\">[docs/walkthrough]</code> line comment. When newcomers come our way and want to learn more about our project, I point them to a GitHub search for that special identifier in the project’s codebase to pull up all those great code comments to learn about the project. (They could also clone the code locally and search for that identifier using their code editor, but a GitHub search is available to everyone.)</p>\n\n<p>Here are some examples of such high-context comments and the different purposes they serve:</p>\n\n<p><strong>Navigation and flow</strong></p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code># [docs/walkthrough]\n# This module is responsible for generating the content to insert in the &lt;head&gt;\n# HTML tag across all pages.\n</code></pre></div></div>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code># [docs/walkthrough]\n# This class is the entrypoint to all output generation. From here, content\n# flows down into controllers, then into serializers and formatters.\n</code></pre></div></div>\n\n<p><strong>Performance</strong></p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code># [docs/walkthrough]\n# This is an example of an implementation that doesn't feel like it's the\n# right way to do it, but after looking at profiles and traces, we know\n# that this implementation performs better than a SQL-only solution.\n#\n# There's currently no good index on the `xyz` table to quickly search\n# for nested values, so it ends up being faster to filter through all\n# values in Ruby than it is to filter those out using a `WHERE`\n# clause at the database level. We're looking into remodeling this data.\n</code></pre></div></div>\n\n<p><strong>Trivia</strong></p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code># [docs/walkthrough]\n# Even though this class usually handles objects of type X, this one\n# is an exception because it can be overridden by people manually creating\n# resource of the same name.\n</code></pre></div></div>\n\n<p><strong>Resiliency</strong></p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code># [docs/walkthrough]\n# Historically, we kept track of the resource count by incrementing a value in a\n# key-value store whenever a resource was created. However, because of scaling\n# constraints, we started tracking this value using a proper data ETL process.\n# We now simply read the count from that data store and cache it aggressively.\n</code></pre></div></div>\n\n<p><strong>Implementation</strong></p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code># [docs/walkthrough]\n# By default, this class (including its subclasses) doesn't expose any method\n# to the outside world. To do so, methods must be marked with the \"world_public\" \n# identifier. This is a safe-by-default way to avoid allocating wrapper objects,\n# while still allowing public methods to be created for internal use.\n</code></pre></div></div>\n\n<p>The benefit of this approach is that the documentation is built into the code, so the risk that it becomes stale or outdated is much lower than if that documentation lived outside of the code itself.</p>\n\n<p>One thing you can play around with is moving up and down levels of abstraction throughout the comments. Some of them may explain why a specific function is implemented this way, while others may explain the business domain history behind this legacy module.</p>\n\n<h3 id=\"sounds-great-how-do-i-get-started\">Sounds great! How do I get started?</h3>\n\n<p>As a starting point, identify existing high-quality comments in your project, and tag them with the special identifier of your choice. Then, look for areas in the codebase that would benefit from having a little bit more context and documentation, and take some time to write clear, helpful comments that you can add that special tag to. Finally, once you have a generous collection of high-quality comments with that special identifier, start pointing newcomers to search for this identifier, and let them soak up all that context.</p>\n\n<p>As the documentation shepherd on your team, always be on the lookout for opportunities to write more of those high-context code comments: you’ll be rewarded for it in the future, either by your colleagues thanking you for writing those comments that helped them step up quickly, or by your boss for reducing the “onboarding load” I mentioned earlier on the rest of the team, who instead were able to focus on the task at hand.</p>\n\n<p>Great software documentation is like a flywheel. It’s hard to write at first, and the return on investment is not immediately visible. But with time, it compounds, and eventually, you’ll wonder how you managed to live without it.</p>\n",
            "url": "https://maximevaillancourt.com/blog/code-walkthroughs",
            "summary": "Welcoming newcomers with high-quality, high-context code comments.",
            "date_published": "2020-07-28 00:00:00 +0000"
            
        },

        {
            "id": "/blog/setting-up-your-own-digital-garden-with-jekyll",
            "title": "Setting up your own digital garden with Jekyll",
            "content_html": "<p><strong>Eager to try the demo of the template?</strong> 👉 <a href=\"https://digital-garden-jekyll-template.netlify.app\" target=\"blank\">digital-garden-jekyll-template.netlify.app</a></p>\n\n<p>Digital gardens and public note-taking spaces are <a href=\"https://twitter.com/ness_labs/status/1262778800649187330\" target=\"blank\">all the rage these days</a>, as they’re a great way to foster an environment where ideas mesh together and others can take inspiration from. You can set up a digital garden of your own in a few minutes, and have your own personal corner of the Internet where you’ll seed and grow ideas.</p>\n\n<p>If you’re familiar with Markdown and/or HTML, you’ll be right at home, as that’s how your notes will be formatted.</p>\n\n<p>The end result will look similar to this:</p>\n\n<p><img src=\"https://user-images.githubusercontent.com/8457808/82400515-7d026d80-9a25-11ea-83f1-3b9cb8347e07.png\" alt=\"\"></p>\n\n<p>If you happen to use <strong>Obsidian</strong> to work on your notes, you’ll likely want to open <a href=\"https://refinedmind.co/obsidian-jekyll-workflow\" target=\"blank\">this guide from Mike</a> and read through it in parallel with this one.</p>\n\n<p>Alternatively, if you use <strong>Roam</strong> and would like to automatically convert your Roam Research backup to a garden using this template, take a look at <a href=\"https://github.com/DoomHammer/roam-to-git/tree/roam-to-garden\" target=\"blank\">this repository</a>.</p>\n\n<p>Without further ado, let’s get started!</p>\n\n<h2 id=\"instructions\">Instructions</h2>\n\n<h3 id=\"0-set-up-prerequisites\">0. Set up prerequisites</h3>\n\n<p>For this tutorial, we’ll need to install a few things on your machine (you may have some of these already). Following the instructions on each website to install them.</p>\n\n<ul>\n  <li><a href=\"https://www.ruby-lang.org/\" target=\"blank\">Ruby</a></li>\n  <li><a href=\"https://rubygems.org/\" target=\"blank\">RubyGems</a></li>\n  <li><a href=\"https://git-scm.com/downloads\" target=\"blank\">Git</a></li>\n</ul>\n\n<p>You’ll also need to create accounts on the following services:</p>\n\n<ul>\n  <li>\n<a href=\"https://github.com/join\" target=\"blank\">GitHub</a> (to store your digital garden files)</li>\n  <li>\n<a href=\"https://app.netlify.com/signup\" target=\"blank\">Netlify</a> (to serve your digital garden website to the world)</li>\n</ul>\n\n<p>Once everything is set up, let’s start creating your own digital garden.</p>\n\n<h3 id=\"1-create-a-fork-of-the-template-repository\">1. Create a fork of the template repository</h3>\n\n<p>To simplify things, I provide the template shown in the image above to get started. You can always tweak this template to your taste later.</p>\n\n<p>Visit the GitHub page for my template repository (<a href=\"https://github.com/maximevaillancourt/digital-garden-jekyll-template\" target=\"blank\"><code class=\"language-plaintext highlighter-rouge\">maximevaillancourt/digital-garden-jekyll-template</code></a>), and fork it to your account using the Fork button:</p>\n\n<p><img src=\"https://help.github.com/assets/images/help/repository/fork_button.jpg\" alt=\"\"></p>\n\n<p>Once the forking process is complete, you should have a fork (essentially a copy) of my template in your own GitHub account. On the GitHub page for your repository, click on the green “Clone or download” button, and copy the URL: we’ll need it for the next step.</p>\n\n<h3 id=\"2-clone-your-repository-locally\">2. Clone your repository locally</h3>\n\n<p>Next, we want to download the files from your GitHub repository onto your local machine. To do this, replace <code class=\"language-plaintext highlighter-rouge\">&lt;YOUR_COPIED_URL_HERE&gt;</code> in the command below with the URL you copied in the previous step, then execute this command:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ git clone &lt;YOUR_COPIED_URL_HERE&gt; my-digital-garden\n</code></pre></div></div>\n\n<p>As a reference point, this is how it looks like for me (the difference is likely just the GitHub username):</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ git clone git@github.com:maximevaillancourt/digital-garden-jekyll-template.git my-digital-garden\n</code></pre></div></div>\n\n<p>Then, navigate into the directory that was just created:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ cd my-digital-garden\n</code></pre></div></div>\n\n<h3 id=\"3-test-out-the-site-locally\">3. Test out the site locally</h3>\n\n<p>Sweet! You now have your repository’s source code on your machine. Within the <code class=\"language-plaintext highlighter-rouge\">my-digital-garden</code> directory, run the following command to install the necessary dependencies like Jekyll:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ bundle\n</code></pre></div></div>\n\n<p>Once that’s done, ask Jekyll to start serving the site locally:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ bundle exec jekyll serve\n</code></pre></div></div>\n\n<p>Then, open up <a href=\"http://localhost:4000\" target=\"blank\"><code class=\"language-plaintext highlighter-rouge\">http://localhost:4000</code></a> in your browser.</p>\n\n<p>If everything’s done correctly, you should now see the home page of your digital garden. 🎉</p>\n\n<p>Keep in mind that this site is only available locally (notice the <code class=\"language-plaintext highlighter-rouge\">localhost</code> part of the URL), so if we want it to be available on the Internet for everyone to enjoy, we need to deploy it to the Internet: we’ll use Netlify for that in the next step.</p>\n\n<h3 id=\"4-connect-your-github-repository-to-netlify\">4. Connect your GitHub repository to Netlify</h3>\n\n<p>Netlify lets you automatically deploy your digital garden on to the Internet when you update your GitHub repository. To do this, we need to connect your GitHub repository to Netlify:</p>\n\n<ol>\n  <li>Log in to <a href=\"https://app.netlify.com/\" target=\"blank\">Netlify</a>\n</li>\n  <li>Once logged in, click the “New site from Git” button</li>\n  <li>On the next page, select GitHub as the continuous deployment provider (you may need to authorize the connection, in which case, approve it)</li>\n  <li>On the next page, select your digital garden repository from the list</li>\n  <li>On the next page, keep the default settings, and click on “Deploy site”.</li>\n</ol>\n\n<p>That was easy! We’re almost done.</p>\n\n<p>Wait a couple of minutes for the initial deploy to complete.</p>\n\n<p>Once that’s done, your digital garden should be available on the Internet via a generic Netlify URL, which you can change to a custom domain later if you’d like.</p>\n\n<p>Now the cool thing is this: whenever you push an update to your GitHub repository, Netlify will automatically deploy your updates to the Internet.</p>\n\n<h3 id=\"5-start-tending-to-your-digital-garden\">5. Start tending to your digital garden</h3>\n\n<p>At this point, you can start updating the files on your machine (in the <code class=\"language-plaintext highlighter-rouge\">my-digital-garden</code> folder) to change your digital garden to your liking: update the copy, add some notes, tweak the layout, customize the colors, etc.</p>\n\n<p>Once you have something you’re happy with, push your changes to your GitHub repository with the following commands:</p>\n\n<div class=\"language-plaintext highlighter-rouge\"><div class=\"highlight\"><pre class=\"highlight\"><code>$ git add --all\n$ git commit -m 'Update content'\n$ git push origin master\n</code></pre></div></div>\n\n<p>If that command succeeds and the rest of the tutorial was done correctly, in a couple of minutes, you should see your changes live on your Netlify website. 🚀</p>\n\n<p>And we’re done! You now have your own digital garden. Take care of your mind and the rest will follow. 🍃</p>\n",
            "url": "https://maximevaillancourt.com/blog/setting-up-your-own-digital-garden-with-jekyll",
            "summary": "Carve out your own space where you’ll seed, cross-pollinate, and grow ideas.",
            "date_published": "2020-05-20 00:00:00 +0000"
            
        },

        {
            "id": "/blog/why-i-use-a-thinkpad-x220-in-2021",
            "title": "Why I still use a ThinkPad X220 in 2021",
            "content_html": "<p><strong>Update (2020-05-18)</strong>: I’ve since switched to a more powerful desktop computer for my photography business. I still use the X220 as a dedicated machine to connect with Zwift, a social cycling app. 🚴</p>\n\n<p><strong>Update (2021-01-17)</strong>: … and I’m back on the X220 full-time! I sold the desktop computer mentioned above. I much prefer the X220’s portability compared to the desktop’s raw power.</p>\n\n<hr>\n\n<p>My personal machine is a 8-year-old Lenovo ThinkPad X220 running Ubuntu 18.04 with i3wm.</p>\n\n<p>It does not have a single USB-C port. It sports a 1366x768 TN display panel (in case you’re wondering, you <em>can</em> see each individual pixel with your bare eyes). The battery life of the 4-cell battery is horrendous (I barely get 2 hours out of this thing). The trackpad is so small, one could even wonder if it’s a trackpad for ants (no need to say that I never use it). The Wi-Fi card only supports 2.4GHz networks, so hopes for blazingly fast Wi-Fi are to be pushed aside. There’s even a bit of gaffer tape on the bottom left corner of the body to hold the cracked plastic together.</p>\n\n<div class=\"img-group\">\n<div class=\"overflow\">\n  <img alt=\"Lenovo ThinkPad X220 with lid open\" src=\"/assets/x220-full.png\">\n</div>\n<div class=\"caption\">Speaking of the devil...</div>\n</div>\n\n<p>At my day job, I’m lucky enough to work on a top-spec MacBook Pro provided by my employer. It has a glorious Retina display, 16GB of RAM, a modern Core i7 CPU, and a huge trackpad to boot. All in all, it’s a pretty fancy machine, one that many people would love to use as a daily driver.</p>\n\n<p>I mean, let’s just compare the trackpads for a second. It’s almost funny at that point.</p>\n\n<div class=\"img-group\">\n<div class=\"overflow\">\n  <img alt=\"Comparison of trackpad size between Lenovo ThinkPad X220 and 15 inch MacBook Pro\" src=\"/assets/x220-mbp-trackpads.png\">\n</div>\n<div class=\"caption\">Trackpad size comparison. ThinkPad X220 on the left. 15\" MacBook Pro on the right.</div>\n</div>\n\n<p>Surprisingly though, out of the two laptops, my favourite is not the MacBook Pro. When I’m at home, I tuck the aluminium slab away and take out the magnesium brick that is the ThinkPad X220.</p>\n\n<p>It’s not pretty. It’s not particularly fast. But it does everything I need, and it’s always ready for everything I throw at it. Could it be a bit of nostalgia for old school hardware? Maybe.</p>\n\n<hr>\n\n<p>I strongly believe a Lenovo ThinkPad X220 is still a terrific laptop to use in 2021 and beyond. It’s not for everyone, but the X220 definitely sparks joy. Plus, its accessible price point makes it almost impossible to ignore. I got mine second-hand (third-hand? fourth-hand? I don’t even know) in great condition for less than 200$ in Canada.</p>\n\n<p>In practical terms: the X220 plays 1080p videos from YouTube wonderfully, renders Portal 2 quite happily (albeit with lower graphics quality than what you may usually enjoy on higher-end machines), and is a perfect machine to dual boot Windows on for maximum value. I spend most of my time in a Web browser or CLI tools, so it’s not like I’m running complex simulations, but still.</p>\n\n<p>The X220, just like any other classic ThinkPad, is extensible, sturdy, reliable, and provides everything I could ask for in a laptop.</p>\n\n<h2 id=\"extensibility\">Extensibility</h2>\n\n<p>The classic ThinkPad laptops have “extensibility” written all over them. Most components are user-replaceable. In “ship of Thesus” fashion, if you individually replace every single component of the X220 one at a time, is it still the same X220?</p>\n\n<p>Seriously though, just look at this list:</p>\n\n<ul>\n  <li>User-replaceable display</li>\n  <li>User-replaceable wireless card</li>\n  <li>User-replaceable keyboard</li>\n  <li>User-replaceable RAM</li>\n  <li>User-replaceable battery</li>\n  <li>User-replaceable 2.5” storage and mSATA</li>\n</ul>\n\n<p>That’s a list many laptop owners can only dream of. Laptops are increasingly shut tight deliberately, preventing users from fixing and/or upgrading their devices themselves. Not with a classic ThinkPad though.</p>\n\n<p>I previously owned a ThinkPad X230, which many consider to be part of the last generation of “classic” ThinkPads. Multiple components of that X230 had been upgraded: IPS display, SSD storage, additional RAM, backlit keyboard in my native language, new 9-cell battery, etc. It was a dream machine, and the X220, just like other classic ThinkPads, offers the same extensibility and user-friendly servicing. I eventually bricked the X230 by spilling water in the underside RAM slot (weird accident, don’t ask).</p>\n\n<p>After bricking the X230, I purchased a second-hand ThinkPad X250 on eBay, only to sell it a few weeks later as it’s a huge step backwards compared to the X220/X230: there’s only one user-accessible RAM slot in the X250 (instead of two). The rest of the RAM is soldered to the board. The keyboard is user-replaceable, but to do so you need to take the entire computer apart (instead of just replacing the keyboard directly as with the X220/X230). Like, what? Who thought that was a good idea?</p>\n\n<h2 id=\"compatibility\">Compatibility</h2>\n\n<p>Being a 2011 laptop, it also features various ports that some modern laptops users may only have heard of. At work, where everyone uses a top-of-the-line MacBook Pro, it’s like some sort of utopia where everything is wireless, and we don’t ever need to use the USB-C ports for anything other than charging the laptops or connecting to a giant 4K display.</p>\n\n<p>In the <em>real</em> world, however, you’d need a handful of adapters with a MacBook Pro to connect to the rest of the world. The X220 provides everything you could practically ask for here:</p>\n<ul>\n  <li>USB-A ports (3 of them!)</li>\n  <li>SD card slot</li>\n  <li>Digital video out (DisplayPort)</li>\n  <li>Analog video out (good old VGA)</li>\n  <li>Ethernet port</li>\n  <li>Kensington lock</li>\n</ul>\n\n<div class=\"img-group\">\n<div class=\"overflow\">\n  <img alt=\"Left hand side of ThinkPad X220, showing two USB-A port, SD card slot, VGA out, DisplayPort, and physical Wi-Fi killswitch\" src=\"/assets/x220-ports.png\">\n</div>\n<div class=\"caption\">Left side of the X220. A whole world of connectivity awaits.</div>\n</div>\n\n<p>Plus, there are other goodies about this machine:</p>\n<ul>\n  <li>7-row keyboard</li>\n  <li>Visual status indicators (Wi-Fi, Bluetooth, battery, storage I/O)</li>\n  <li>Physical Wi-Fi killswitch</li>\n  <li>ThinkLight above the screen for late-night hacking sessions</li>\n</ul>\n\n<h2 id=\"reliability\">Reliability</h2>\n\n<p>I like to think that a classic ThinkPad is akin to a Toyota Corolla, one of the most (if not the most) reliable production cars ever produced. Give it a good and thorough clean up once a year, change the oil at regular intervals, keep your software up-to-date, and you’ll enjoy this machine for a long time.</p>\n\n<p>Classic ThinkPads just <em>feel</em> like business. They won’t let you down. The <a target=\"blank\" href=\"https://www.reddit.com/r/thinkpad/\">/r/thinkpad</a> sub-reddit is full of classic ThinkPads (some of them I would even call “retro” instead of “classic”), and these things just keep on running, decades after the initial release date.</p>\n\n<p>The laptop’s shell is made out of magnesium instead of plastic, making it extra sturdy. The keyboard feels <em>great</em>. Not your typical cheap keyboard from your run-of-the-mill HP laptop. The display hinges are solid.</p>\n\n<p>Again, going back to the X250 I used for a couple of weeks: it felt cheap compared to the X220/X230. The shell was made out of plastic, the display seemed fragile, and the trackpoint buttons felt flimsy. Not a great experience coming from a X230.</p>\n\n<p>That’s when I knew I’d go for the X220, and stay for a while.</p>\n\n<hr>\n\n<p>If you’re looking to purchase a second-hand classic ThinkPad, do it. Buy the thing. Slap GNU/Linux distribution on there (or *BSD, if you’re into that sort of thing), and have fun.</p>\n",
            "url": "https://maximevaillancourt.com/blog/why-i-use-a-thinkpad-x220-in-2021",
            "summary": "Extensibility, compatibility, and reliability.",
            "date_published": "2019-11-24 00:00:00 +0000"
            
        },

        {
            "id": "/blog/sleep-activation-energy",
            "title": "On sleepiness, activation energy, and flow",
            "content_html": "<p>I noticed something noteworthy a few weeks ago.</p>\n\n<p>It seems that if I sleep a little less than what is usually recommended, say approximately 6 hours instead of the usual 7-9 hours, I will find it easier to get started on tasks and keep the ball rolling throughout the day. In other words, sleeping less makes me more productive.</p>\n\n<p>To be clear, this observation does not fit in the overall picture painted by modern science, which notes that adequate sleep leads to improved health and productivity.</p>\n\n<p>Now, what I observe is more than likely to be a false impression—a mere <em>feeling</em> that I’m more productive when really I’m just as productive as usual or even less so—but it’s something I’ve been able to reproduce often enough to feel like there must be something going on here, as it’s something I can temporarily leverage and benefit from. It’s also worth pointing out that this very biased, non-scientific experiment on a sample size of 1 should not be taken seriously. I’m simply documenting what I’ve observed.</p>\n\n<hr>\n\n<p>In chemistry, there’s a concept called “activation energy”, which is the quantity of energy that must be provided to a system in order to generate a reaction. Unless a system is supplied enough energy to pass the threshold required to get things moving, the system won’t budge, and will remain inactive.</p>\n\n<p>Imagine standing at the base of a hill with a ball at your feet (this is the system). You want to kick the ball to your friend over on the other side on the mountain (this is the reaction). If you kick the ball gently, it will roll up the mountain for a few meters, then roll back down to your feet—insufficient activation energy to trigger the reaction. Only if you kick the ball strong enough will it reach the top of mountain and roll over to the other side.</p>\n\n<div style=\"max-width: 30em; margin: 3em auto;\">\n\n<img src=\"/assets/energy.svg\">\n\n</div>\n\n<p>Drawing a parallel with psychology and human behavior, we can use activation energy as a mental model for motivation and procrastination.</p>\n\n<p>When faced with a given task, the human brain must come up with the required activation energy to get started and get the ball rolling onto the other side of the mountain. Without it, we remain still, unable to get started for a long period of time.</p>\n\n<hr>\n\n<p>Analysis paralysis. Indecision &amp; inaction. Overthinking and not moving. These are all related.</p>\n\n<p>I sometimes get stuck and paralyzed by overthinking and overanalyzing a system. This indecision leads to inaction, why means I’m stuck at square one where I continue analyzing the situation, hoping eventually I’ll have enough information to get started.</p>\n\n<p>In my experience, getting less sleep leads to a natural ability to short the above circuit and get moving immediately, while also reducing the time to reaching a state of flow.</p>\n\n<p>If I dig in a little more, it seems to have to do with a desire to get it over with whatever task is at hand, and a drive to move on to the next thing quickly. This, combined with a willingness to be more <del>scrappy</del> resourceful as time passes, leads to a practical sense of acceleration that I haven’t been able to replicate in any other way.</p>\n",
            "url": "https://maximevaillancourt.com/blog/sleep-activation-energy",
            "summary": "Parallels between chemistry and the human mind.",
            "date_published": "2019-09-30 00:00:00 +0000"
            
        },

        {
            "id": "/blog/flash",
            "title": "I didn’t know any better",
            "content_html": "<p>One day, when I was in elementary school (mid-2000s), I decided I would create a personal website to share games, news, and other cool tidbits of my life with my friends and family. I also wanted it to be password protected so that only my friends would see it, and to prevent big bad internet strangers from seeing private information (oh how times have changed).</p>\n\n<p>However, I didn’t know anything about HTTP authentication, HTML forms, databases, or security best practices. I couldn’t care less, and figured I would find a way. I <em>was</em>, after all, in elementary school. Like, 10 years old maybe.</p>\n\n<p>So I set out to build that website, and used the one technology I knew could achieve this: Flash.</p>\n\n<p>That’s right, good old <code class=\"language-plaintext highlighter-rouge\">.swf</code> files and all.</p>\n\n<p>I first implemented the password protection this way: within the Flash view, there was a login screen with a single field where my friends would type in the password. Naturally it was a hardcoded password, straight in the .swf, because <em>why the hell not</em>, I’m 10 years old, this is fine.</p>\n\n<p>What is this sound I hear? Ah, yes, that’s the sound of security engineers from around the world screaming in unison. Yes. I know. Again, I was 10 years old.</p>\n\n<p>I encountered a problem pretty early though: what if I wanted to show different things to different people? Hmm.</p>\n\n<p>Instead of sharing the protecting the website with the same password for all my friends and family members, I would need them to each have their own password (because duh, <em>security</em>), and on top of that, surface different things to different people. So…</p>\n\n<p>Again, with the infinite creativity of a 10-year-old who has no idea what they’re doing, I found a brilliant solution:</p>\n\n<p>Create a separate .swf file for <em>each of my friends</em>, with a different hardcoded password in every of them, and serve them all as different files on my ISP-provided web server.</p>\n\n<p>YES. I KNOW.</p>\n\n<p>Amazing, isn’t it?</p>\n\n<p>I just didn’t know any better.</p>\n",
            "url": "https://maximevaillancourt.com/blog/flash",
            "summary": "A security horror story starring 11-year-old me… and Flash.",
            "date_published": "2019-02-28 00:00:00 +0000"
            
        },

        {
            "id": "/blog/usb-drop-olevba",
            "title": "Inspecting a “USB Drop” Attack Using olevba.py",
            "content_html": "<p><img src=\"https://cdn-images-1.medium.com/max/800/1*r8-nLE9X_-HqKsiQ3w6u1w.png\" alt=\"\"></p>\n\n<p><strong>TL;DR: Never plug USB keys you find laying around in your computer.</strong> They may\ncontain malware that silently deploys as you plug it in, stealing sensitive\ninformation and downloading viruses in the background.</p>\n\n<hr>\n\n<p><em>For those of you who may know me, I’m the kind of guy who runs Linux on his\ncomputer full-time (except for the occasional music production or video editing\nsession). This means no Windows vulnerability to worry about, no random update\nthat starts while I’m working… and also no Microsoft Office suite. This is\nimportant to the story, so keep it in mind.</em></p>\n\n<p>It all started with a regular old bright orange USB key that was left laying\naround on a meeting room table.</p>\n\n<p>The key itself wasn’t identified and did not appear to give away any information\nabout its owner, so I thought I would just do my good deed of the day and try to\nfind some sort of <code class=\"language-plaintext highlighter-rouge\">IF_FOUND.txt</code> file to <strong>identify its owner and give it back\nto them</strong>. So I proceeded to boot up my Linux partition (you can never be too\ncautious!), then plugged in the key.</p>\n\n<p>At first glance, <strong>the key contained seemingly important (and sensitive!)\ninformation about the company’s assets and strategic planning</strong>. I first\nwondered why anyone would think it safe to carry such valuable documents on a\nfriggin’ unencrypted USB key (and most importantly, forgetting it in a meeting\nroom). Then, in my attempt to identify the owner of the key, I opened a random\nfolder and started browsing through the files.</p>\n\n<p>A few seconds in, two oddidities made me realize that something wasn’t quite\nright.</p>\n\n<p>First, all the documents on the key had the <code class=\"language-plaintext highlighter-rouge\">.docm</code> extension, which are files\nknown as “Office Word Documents with Macros”, basically meaning that they’re\nWord documents with Visual Basic for Applications (VBA) code baked in. In Excel\nspreadsheets and Access databases especially, VBA Macros allow for improved\nfunctionality and useful sequences of actions that can be used to automate\notherwise mundane tasks.</p>\n\n<p>The other thing that ticked me off was the <em>Date modified</em> attribute of the\nfiles on the key: they were all set to the exact same date. I mean, yes, the\nfiles could all have been copied together and their <em>Date modified</em> fields\nresetted to the same moment, but still. It just didn’t feel right.</p>\n\n<p>The combination of the way the key was left on the table AND the two oddities I\nfound led me to think about a completely different scenario:</p>\n\n<p><strong>This wasn’t a regular old “normal” USB key. Somebody was trying to fool me,\nand I needed to delve deeper.</strong></p>\n\n<hr>\n\n<p>I took the risk of opening one of the Word documents using LibreOffice (with\nmacros disabled, of course), but was left disappointed when I saw that the\ndocument was completely empty. I tried opening another file, hoping to see\nsomething in there, but alas, it was completely empty as well.</p>\n\n<p>The rabbit hole was deepening yet again.</p>\n\n<hr>\n\n<p>Thinking about the <code class=\"language-plaintext highlighter-rouge\">.docm</code> extension, I quickly understood that the documents\nprobably contained malicious VBA code, but I did not know exactly what kind of\nstuff I was dealing with. I figured I would just keep digging until I would find\nsomething. After a quick Google search, I stumbled upon the excellent\n<code class=\"language-plaintext highlighter-rouge\">olevba.py</code> tool, part of the\n<a href=\"https://www.decalage.info/python/oletools\" target=\"blank\">oletools</a> collection, which parses\nMS Office documents to detect VBA macros then extract their source code.</p>\n\n<p>Starting with MS Office 2007, Word documents are really just OpenXML files\nwrapped in <code class=\"language-plaintext highlighter-rouge\">.doc</code>/<code class=\"language-plaintext highlighter-rouge\">.docm</code>/ <code class=\"language-plaintext highlighter-rouge\">.docx</code> extensions, so after extracting one of the\ndocuments and browsing through the extracted files, I found an interesting\nbinary file which was significantly larger than the others, appropriately named\n<code class=\"language-plaintext highlighter-rouge\">vbaProject.bin</code>. I was pretty confident that this was the file that contained\nmalicious VBA code, probably downloading malware from a remote server and\nexecuting it on the victim’s machine (in this case, my computer, but since I was\nnot on Windows, the chances of being attacked were pretty slim).</p>\n\n<p>However, extracting the source of the <code class=\"language-plaintext highlighter-rouge\">vbaProject.bin</code> file using the\n<code class=\"language-plaintext highlighter-rouge\">olevba.py</code> tool gave me a completely different answer:</p>\n\n<script src=\"https://gist.github.com/maximevaillancourt/7758699eaee9c3fbae1b1b7445588c42.js\"></script>\n\n<p>Huh. So this was not malware, after all. This Word document was simply sending\nthe hostname and username to a remote server to identify who had plugged in the\nrogue USB key and opened one of its files. I then realized that <strong>this was an\ninfosec effort to identify imprudent employees who used the (intentionally left\nbehind) USB keys</strong> (and who should never do that, unless they absolutely know\nand trust the USB key).</p>\n\n<p>I was pleasantly surprised to see that the subroutine actually used <code class=\"language-plaintext highlighter-rouge\">MsgBox</code> to\ncreate a popup announcing to the imprudent user that “the file is corrupted and\ncannot be opened”.</p>\n\n<p><strong>Long story short: only use USB keys you know and trust.</strong></p>\n",
            "url": "https://maximevaillancourt.com/blog/usb-drop-olevba",
            "summary": "Or, “why you shouldn’t pick up random USB keys”.",
            "date_published": "2017-04-07 00:00:00 +0000"
            
        },

        {
            "id": "/blog/kodekpl",
            "title": "“Who is this KodekPL pushing commits to my GitHub repo?”",
            "content_html": "<p>That's what I thought when I saw \"KodekPL\" as the author of a commit in my repo... A commit I had just pushed myself seconds before. But why KodekPL and not someone else? <em>Who is this guy?</em></p>\n\n<p>I’m willing to bet you play Minecraft. A lot of Minecraft. Enough to warrant the need of creating a server. Perhaps one day you downloaded Spigot. “Wait a second here, why are we talking about Minecraft? I’m talking about my GitHub repo here. Someone is in my repo! What the heck!?”, I hear you say.</p>\n\n<p>Turns out, there’s a link between Spigot and KodekPL on GitHub.</p>\n\n<p>Upon further investigation, <a href=\"https://api.github.com/users/KodekPL/events/public\" target=\"blank\">KodekPL’s GitHub email address</a> is “unconfigured@null.spigotmc.org”. Git, for some reason, picked that up as your email address, hence the wrong author on GitHub. Setting the global variables fixes this.</p>\n\n<figure class=\"highlight\"><pre><code class=\"language-bash\" data-lang=\"bash\">git config <span class=\"nt\">--global</span> user.email your@email.com\ngit config <span class=\"nt\">--global</span> user.name <span class=\"s2\">\"Your Name\"</span></code></pre></figure>\n\n<p>That’s it. Push something to GitHub and enjoy!</p>\n",
            "url": "https://maximevaillancourt.com/blog/kodekpl",
            "summary": "Seriously, who’s that guy, and how is he committing to my repository?",
            "date_published": "2015-12-22 00:00:00 +0000"
            
        }

    ]
}
