<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Posts on Claudio Ortolina</title><link>https://claudio-ortolina.org/posts/</link><description>Recent content in Posts on Claudio Ortolina</description><generator>Hugo -- gohugo.io</generator><language>en</language><copyright>&lt;p xmlns:dct="http://purl.org/dc/terms/" xmlns:cc="http://creativecommons.org/ns#" class="license-text">&lt;a rel="cc:attributionURL" property="dct:title" href="https://claudio-ortolina.org">This website&lt;/a> by &lt;a rel="cc:attributionURL dct:creator" property="cc:attributionName" href="https://claudio-ortolina.org">Claudio Ortolina&lt;/a> is licensed under &lt;a rel="license" href="https://creativecommons.org/licenses/by/4.0">CC BY 4.0&lt;/a>&lt;/p></copyright><lastBuildDate>Sun, 14 Sep 2025 11:42:42 +0000</lastBuildDate><atom:link href="https://claudio-ortolina.org/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>A great music system</title><link>https://claudio-ortolina.org/posts/a-great-music-system/</link><pubDate>Fri, 08 Mar 2024 09:00:00 +0100</pubDate><guid>https://claudio-ortolina.org/posts/a-great-music-system/</guid><description>
&lt;img src="https://claudio-ortolina.org/img/a-great-music-system/cover.png"/>
&lt;p>Over the last few months I had the opportunity to spend some time ironing out a music system that would marry analog and digital listening, log everything I play for recommendations and tracking of future releases, and collect all physical releases I own along with accompanying notes and reflections.&lt;/p>
&lt;h2 id="music-library">
Music library
&lt;a href="#music-library" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>I own music in different formats:&lt;/p>
&lt;ul>
&lt;li>Vinyl records&lt;/li>
&lt;li>CDs&lt;/li>
&lt;li>High-resolution files (usually in &lt;a href="https://en.wikipedia.org/wiki/FLAC">FLAC&lt;/a> format)&lt;/li>
&lt;/ul>
&lt;p>Why 3 formats?&lt;/p>
&lt;ul>
&lt;li>Vinyl is usually the best format for old records before the 1980s, as it was the dominant medium. For a lot of music, there’s literally no other available physical format. The format had a massive revival in the last few years, and many artists release high-quality, 180g vinyls which sound amazing.&lt;/li>
&lt;li>CDs tend to be the target format for music produced in the late 1980s up to the 2010s, before streaming took off. A FLAC digital file is absolutely fine as a replacement for a CD quality-wise, but I’m a sucker for booklets and limited edition goodies. Particularly in the UK, second-hand CDs are sold in pretty much any charity shop, and more often than not you can get very good deals.&lt;/li>
&lt;li>Digital files are convenient for listening on the go, and are also sometimes the only option for bands who don’t produce physical releases. Both record labels and websites like &lt;a href="https://bandcamp.com">Bandcamp&lt;/a> give the option to purchase in FLAC format.&lt;/li>
&lt;/ul>
&lt;p>What’s crucial here is that for music I really love, I do wanna own it for a few reasons:&lt;/p>
&lt;ul>
&lt;li>For new releases, I want to compensate the artist or band, and the best way is to buy their music (and attend their gigs).&lt;/li>
&lt;li>Streaming platforms can pull content anytime, and in many cases they don’t have all releases I want to listen to.&lt;/li>
&lt;li>Once I have albums in my library, I can organise them how I prefer (both physically and digitally).&lt;/li>
&lt;/ul>
&lt;p>A priceless extra is that my 2-year old daughter loves CDs because they look like little books but they make music, and vinyls because they’re big and rotate.&lt;/p>
&lt;h2 id="hardware">
Hardware
&lt;a href="#hardware" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;img src="https://claudio-ortolina.org/img/a-great-music-system/music-system.png" alt="A photo of the music system I use showing the R5 player unit and the Pro-ject E1 turntable" class="left" />
&lt;p>There are a few moving parts:&lt;/p>
&lt;ul>
&lt;li>A &lt;a href="https://www.synology.com/en-uk">Synology NAS&lt;/a> storing all digital files.&lt;/li>
&lt;li>A &lt;a href="https://www.ruarkaudio.com/products/r5-high-fidelity-music-system">Ruark R5&lt;/a> music system (which can play CDs directly, and that has an integrated amplifier for the turntable). It’s been recently discontinued, which does make a bit uneasy, but the manufacturer still supports it and I reckon I can go on for a few years before having to evaluate a replacement.&lt;/li>
&lt;li>A &lt;a href="https://www.project-audio.com/en/product/e1/">Pro-Ject E1&lt;/a> turntable, playing through the R5. Pricy, but it really just works.&lt;/li>
&lt;li>A &lt;a href="https://www.raspberrypi.com/products/raspberry-pi-4-model-b/">Raspberry Pi 4&lt;/a> with a &lt;a href="https://www.hifiberry.com/shop/boards/hifiberry-dac2-pro/">HifiBerry DAC2 Pro&lt;/a> hat, which allows the PI to play audio with great quality via the R5.&lt;/li>
&lt;/ul>
&lt;img src="https://claudio-ortolina.org/img/a-great-music-system/raspberry-pi.png" alt="A photo of the Raspberry Pi 4 with the HifiBerry DAC2 Pro hat" class="left" />
&lt;p>None of this is cheap and there’s certainly more affordable options particularly if you’re able to source a different speakers/amp/turntable combo. The big advantage of using a Raspberry Pi (or an equivalent off the shelf solution) is that you separate “brain” from “muscle” (and the Pi + DAC hat are relatively cost-effective).&lt;/p>
&lt;h2 id="playing-music">
Playing music
&lt;a href="#playing-music" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>The whole system revolves around Plex: the NAS runs &lt;a href="https://www.plex.tv/en-gb/personal-media-server/">Plex Media Server&lt;/a>, while the Raspberry Pi runs &lt;a href="https://www.plex.tv/plexamp/">PlexAmp&lt;/a> headless. My laptop and phone run PlexAmp as well.&lt;/p>
&lt;p>Setting up PlexAmp headless on the Pi required minimal work thanks to the excellent &lt;a href="https://github.com/odinb/bash-plexamp-installer">bash-plexamp-installer&lt;/a> project, which makes it a breeze to both install and update the software and correctly configure the DAC2 Pro hat. Couple of reboots and everything worked like a charm (and still does).&lt;/p>
&lt;p>With this setup, I can use any device to access my music collection, and play it through the Pi if I’m at home, or on-device if I’m out.&lt;/p>
&lt;p>The Plex Media Server instance is configured to scrobble to &lt;a href="https://last.fm">Last.fm&lt;/a>, so everything gets logged automatically no matter where I play it from.&lt;/p>
&lt;p>When I play physical releases, I use the excellent &lt;a href="https://openscrobbler.com">OpenScrobbler.com&lt;/a> web application to search for the album I’m playing, and scrobble it.&lt;/p>
&lt;p>I’m now trying out &lt;a href="https://tidal.com">Tidal&lt;/a> as a way to listen before buying, as it provides hi-fi quality, pays artists better than other platforms, and it’s deeply integrated into Plex (which means I don’t need to use a separate app, and can rely just on PlexAmp).&lt;/p>
&lt;p>If the Tidal experiment is a success, I’ll stop my subscription to Apple Music.&lt;/p>
&lt;p>When music is played by the Pi, the R5 system has no information on what&amp;rsquo;s currently playing. I can read that on my phone, but for other people in the house I programmed a small automation that can be run with Siri, so that anyone can ask &amp;ldquo;What&amp;rsquo;s playing?&amp;rdquo; and get a good answer. This took some lightweight reverse engineering of the PlexAmp web application, and I&amp;rsquo;m not expecting it to be rock solid as it&amp;rsquo;s based on private APIs.&lt;/p>
&lt;p>In short, the PlexAmp web application polls an endpoint that returns a fairly comprehensive playing status with metadata information about the artist being played, both for local music in the Plex Server and on Tidal. As PlexAmp is written in Node.js, I wrote another small Node.js application to poll the same endpoint, and parse the information I need. I then created an iOS shortcut to hit my Node.js application, and read out the response in a human readable format. The shortcut is automatically available on all devices, and can be run via Siri by its name.&lt;/p>
&lt;h2 id="discovery">
Discovery
&lt;a href="#discovery" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>Algorithmic recommendations are now very precise - no matter the platform. I’ve come to find them &lt;em>too precise&lt;/em> in the sense that they don’t stray off the beaten path.&lt;/p>
&lt;p>I prefer to monitor different sources, particularly when it comes to my core preference, progressive rock:&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://www.loudersound.com/prog">Prog Magazine&lt;/a>, which is an old-school monthly zine (both printed and digital) that covers pretty much anything happening in the progressive scene. I have a digital subscription, and read it on the iPad where I can take notes and add albums to a queue.&lt;/li>
&lt;li>The reviews RSS feed for &lt;a href="https://progarchives.com">progarchives.com&lt;/a>, which is a fairly old progressive rock community website where people publish reviews of whatever album they can think of. The RSS format is great because I can read the review, see the cover art (which does a lot for me), and from that decide if I’m interested to explore more.&lt;/li>
&lt;li>The occasional visit to a few subreddits, just to get a feel of the community is looking at.&lt;/li>
&lt;li>Newsletters from specific record labels that tend to publish music I like (e.g. &lt;a href="https://kscopemusic.com">Kscope&lt;/a>) or &lt;a href="https://www.karismarecords.no/">Karisma Records&lt;/a>)&lt;/li>
&lt;li>&lt;a href="https://thealbumyears.com/">The Album Years&lt;/a> podcast with Steven Wilson and Tim Bowness, which provides a &lt;strong>very opinionated&lt;/strong> list of significant music releases roughly from the 70s till the 90s. Only downside is that it’s very UK-centric, and I wish there were similar lists for other parts of the world.&lt;/li>
&lt;/ol>
&lt;p>Once I’ve listened to something, and the artist is scrobbled to Last.fm, I’m able to monitor new releases via the excellent &lt;a href="https://apps.apple.com/us/app/musicharbor-track-new-music/id1440405750">MusicHarbor&lt;/a> iOS app. The app imports the list of artists I have on Last.fm, and keeps track of all releases by these artists. The import process is easily done manually via a few button presses in the app, and I do that once a month.&lt;/p>
&lt;p>Each Friday (release day!) I get a new batch of albums to check out, and I add whatever picks my attention to my queue.&lt;/p>
&lt;h2 id="physical-collection-management-and-notes">
Physical collection management and notes
&lt;a href="#physical-collection-management-and-notes" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>Managing a physical collection is a relatively solved problem: with &lt;a href="https://discogs.com">Discogs&lt;/a>, one can simply scan or search for the correct release, and add it to their own account.&lt;/p>
&lt;p>While this is great to track value and conditions of the item, it&amp;rsquo;s not geared towards managing a collection with associated notes about the artist(s), or lyrics. It&amp;rsquo;s also not possible to arbitrarily draw connections between albums.&lt;/p>
&lt;p>I&amp;rsquo;m currently trying out &lt;a href="https://obsidian.md/">Obsidian&lt;/a> with the &lt;a href="https://github.com/mProjectsCode/obsidian-media-db-plugin">MediaDB&lt;/a> plugin. This lets me quickly add every album via the &lt;a href="https://musicbrainz.org/">MusicBrainz&lt;/a> search API (without matching to an exact release, i.e. you can&amp;rsquo;t pick the correct year/label/country/limited edition).&lt;/p>
&lt;p>Each album becomes its own file with a set of metadata information I can query through a couple of plugins. On top of that, I can write notes and associate albums with simple internal links.&lt;/p>
&lt;p>As the data is stored as markdown files with a YAML front matter, there’s no risk of lock-in, and if I ever need to process the data for further analysis or visualization, I can write my own program that does that.&lt;/p>
&lt;p>Obsidian is available on all platforms I use, and syncs both data and settings without having to do anything special.&lt;/p>
&lt;h2 id="too-much">
Too much?
&lt;a href="#too-much" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>I often ask myself if this system is too complicated, but I personally find that the experience of using it is simple at the expense of a reasonable amount of hidden complexity. I’m also aware that my requirements are many, and that’s because music is pretty much my only significant hobby. It still gives me the same joy I felt as a teenager, and still manages to surprise me even when I think I heard it all.&lt;/p></description></item><item><title>Arriving Somewhere</title><link>https://claudio-ortolina.org/posts/arriving-somewhere/</link><pubDate>Fri, 23 Jun 2023 17:34:25 +0300</pubDate><guid>https://claudio-ortolina.org/posts/arriving-somewhere/</guid><description>
&lt;img src="https://claudio-ortolina.org/img/arriving-somewhere/cover.jpg"/>
&lt;p>I&amp;rsquo;m almost 40 - and while my life has somehow the shape that I envisioned as I grew up, there are definitely big differences. Think about the same picture, but in different colors.&lt;/p>
&lt;p>There&amp;rsquo;s music that accompanied me for the last 20 years, pretty much since I became an adult. It&amp;rsquo;s been long enough that I don&amp;rsquo;t exactly remember when it entered my life, and in some ways it feels like it&amp;rsquo;s always been there.&lt;/p>
&lt;p>Of all that music, thousands of songs, there&amp;rsquo;s a handful that made a deep mark that I still carry with me. More than anything else, one song stands above any other: &lt;em>Arriving Somewhere, But Not Here&lt;/em> by Porcupine Tree.&lt;/p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/Kpeip2B8l2E?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;p>I turned 39 a few days ago and my wife got me as a gift a very nice edition of &lt;em>Deadwing&lt;/em>, the album the song is included in. It&amp;rsquo;s a 2018 remaster, which I&amp;rsquo;m listening to on a high-quality system.&lt;/p>
&lt;p>It&amp;rsquo;s a 12 minute song, with long instrumental sections, and I know every single little twist - yet this new version has some little extra details, some of the ambient parts are crisper, some of the guitar sounds pop a little bit more. I love it.&lt;/p>
&lt;p>It also dawned on me that it is a song about how life turns out different than you think - with fairly tragic images - but with a growing sense of peace as notes go by.&lt;/p>
&lt;p>There is relief in the understanding that some pieces of someone&amp;rsquo;s life are set - that some of the choices made are points of no return. That some dreams are over, and that&amp;rsquo;s ok.&lt;/p>
&lt;p>I do have the feeling of having arrived somewhere, but not here. It is different, but it&amp;rsquo;s equally beautiful. And I&amp;rsquo;m finally ok with that.&lt;/p>
&lt;p>And through the twists and turns of the last 20 years, I have a song that feels like an old friend that grew up with me, or even that was a little bit ahead, and I finally caught up.&lt;/p>
&lt;p>To the next 20 years - hopefully I&amp;rsquo;ll get somewhere.&lt;/p></description></item><item><title>An iPad retrospective</title><link>https://claudio-ortolina.org/posts/ipad-retrospective/</link><pubDate>Thu, 11 Aug 2022 06:34:25 +0300</pubDate><guid>https://claudio-ortolina.org/posts/ipad-retrospective/</guid><description>
&lt;p>I’ve been working on iPad full-time for a while now, and it’s always a source of surprise when I talk about it with other people. This post aims at summarising the &lt;em>why&lt;/em> behind this preference of mine.&lt;/p>
&lt;h2 id="planting-a-seed">
Planting a seed
&lt;a href="#planting-a-seed" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>I spent 2019 on the Turkish West Coast, in a small town near Izmir. It’s a lovely place where everyone knows each other, and where we now have friends and people we care about.&lt;/p>
&lt;p>I was working as a backend engineer on &lt;a href="https://pspdfkit.com/guides/server/pspdfkit-server/overview/">PSPDFKit Server&lt;/a>, a product written in Elixir and packaged via Docker, on my trusty old 2014 11” Macbook Air. A wonderful machine - powerful for its size, and featherweight.&lt;/p>
&lt;p>Around April, a few things happened at the same time:&lt;/p>
&lt;ul>
&lt;li>Spring came: warm, sunny, and ideal to start working outside in the shade of a café.&lt;/li>
&lt;li>A set of requirements that involved way more work with Docker, and consequently a much larger use of CPU, battery, and internet bandwidth.&lt;/li>
&lt;li>The need to finally increase my editor font size to 14pt.&lt;/li>
&lt;/ul>
&lt;p>It quickly became apparent that I was asking too much out of my laptop.&lt;/p>
&lt;p>As I’ve been writing software in the shell for years, I switched to a remote setup using a powerful remote server. 32 cores, very large bandwidth, almost the same experience as working locally (if you take into account the occasional latency problem, well mitigated by mosh).&lt;/p>
&lt;p>Screen size, however, became an issue: 14pt on a 11” screen is crammed, and difficult to use.&lt;/p>
&lt;p>I thought about purchasing a new laptop, but doing that from Turkey proved to be difficult, because of the different keyboard layout.&lt;/p>
&lt;p>I decided I would try a week long experiment using the other device I had, a 3rd generation 12.9 iPad Pro (with its keyboard) which I mainly used for entertainment. I figured that given my remote development setup, I could still get things done.&lt;/p>
&lt;p>The experience was great: I loved the screen size and ratio, and the retina screen made text very easy to read (to the point that I could go back to 12pt, which worked really well in terms of information density). I could work the entire day without worrying about the battery, and enjoy the spring/summer season.&lt;/p>
&lt;p>While coding was (surprisingly) pleasant considering the fiddly setup, other activities were proving to be more difficult: apps like Slack and Google Docs were (and in some cases still are) cumbersome to use, with very little support for keyboard shortcuts, and of substantial inferior quality compared to their desktop counterparts. I got by, occasionally dusting off the Macbook Air for specific tasks that wouldn’t work at all on the iPad (e.g. work that required using the Chrome Web Inspector).&lt;/p>
&lt;p>Then February 2020 came, we moved back to England and the pandemic started. Like many others, we spent most of our time at home, where I could use a decent Mac Mini with a nice keyboard and external screen.&lt;/p>
&lt;p>Despite this change, a seed had been planted in my head: could an iPad eventually replace a desktop computer altogether for a “hardcore” user like me?&lt;/p>
&lt;h2 id="unexpected-changes">
Unexpected changes
&lt;a href="#unexpected-changes" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>Around spring 2020, my role changed, and I started managing a team. Coding became a past-time activity that I kept maintaining for fun, and 99% of my work now involved planning, communication with people, and some form of documents and task lists.&lt;/p>
&lt;p>In December 2020, a family situation forced us to quickly pack our bags and fly to Turkey again. I ended up taking with me all devices (which sounds ridiculous - &lt;em>I know&lt;/em>).&lt;/p>
&lt;p>The following months are a blur - I remember working from all sorts of places: the car, parking lots, hospital cafés, the balcony table, hotel rooms. I once had a meeting in a service station.&lt;/p>
&lt;p>That’s how the iPad came back in full force: always on, always charged, with data always in sync. Pair it with the Apple Pencil, and a pair of earphones, and it gave me the ability to adapt work to a complicated personal life.&lt;/p>
&lt;p>I was happy to see that software had gotten better: Safari gained a desktop mode which supported all sorts of complex web apps, and all other apps improved to the point of being &lt;em>at least&lt;/em> usable (Google Docs, I’m looking at you). Notable mentions go to &lt;a href="https://www.agenda.com">Agenda&lt;/a>, &lt;a href="https://www.mindnode.com">Mindnode&lt;/a>, and &lt;a href="https://flexibits.com/fantastical">Fantastical&lt;/a>, which set a very high bar for quality software in general.&lt;/p>
&lt;p>Later in the year, things started to calm down and even if I could use the Mac Mini, I would prefer the iPad. I invested in a desk stand which would let me prop it to a decent height, connect it to a good keyboard and mouse, and happily went about my work day.&lt;/p>
&lt;img src="https://claudio-ortolina.org/img/an-ipad-retrospective/my-desk.jpg" alt="Photo of my desk showing the iPad with its stand, keyboard, and trackball" class="left" />
&lt;h2 id="introspection">
Introspection
&lt;a href="#introspection" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>I started asking myself what is it about working on the iPad that I love. What started as a challenge motivated by specific constraints evolved now into a solid preference.&lt;/p>
&lt;p>I don’t think I’m making a point - as in trying to be clever or different by not using a traditional computer. There’s plenty of people who do that, and if you search on YouTube you will find all sorts of tutorials on how to try and work around iPadOS’s limitations (e.g. having a Raspberry PI as a sidecar mini-server) to achieve things that are indeed way easier with a laptop.&lt;/p>
&lt;p>The iPad fits my mental model of getting things done (tasks instead of files), and it doesn’t get in the way. It’s got great security defaults, it’s encrypted, and automatically backed up. If I buy a new one, I can easily transfer all my data and be up and running in a couple of hours.&lt;/p>
&lt;p>The iPad lets me focus on what I want to achieve, as opposed to losing myself in endless maintenance.&lt;/p>
&lt;p>I take notes of points of friction, and every once in a while, I review my workflows: if I wanna change anything, I’ll test a different application (after making sure my data is portable), and move on.&lt;/p></description></item><item><title>Teaching Elixir</title><link>https://claudio-ortolina.org/posts/teaching-elixir/</link><pubDate>Tue, 06 Jul 2021 08:34:25 +0300</pubDate><guid>https://claudio-ortolina.org/posts/teaching-elixir/</guid><description>
&lt;p>When I joined &lt;a href="https://pspdfkit.com">PSPDFKit&lt;/a> in 2018, I inherited the development and maintenance of &lt;a href="https://pspdfkit.com/guides/server/pspdfkit-server/overview/">PSPDFKit Server&lt;/a>, the server-side component of &lt;a href="https://pspdfkit.com/pdf-sdk/web/">PSPDFKit for Web&lt;/a>.&lt;/p>
&lt;p>Initially, I was the only person working on the project full-time. The team eventually expanded to a total of three people and I moved on to manage the Web Team.&lt;/p>
&lt;p>In addition, other developers occasionally contributed features with differing degrees of complexity (ranging from small, incremental changes to large features over a few weeks of work).&lt;/p>
&lt;p>Looking at the entire process, I can safely say that onboarding has been smooth and fast, especially for internal hires from different teams.&lt;/p>
&lt;p>There are, of course, lessons learnt from this process, which is exactly what this blog post is about.&lt;/p>
&lt;blockquote>
&lt;p>This post was sponsored by digital product consultancy &lt;a href="dockyard.com">DockYard&lt;/a> to support the Elixir community and to encourage its members to share their stories.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-organization">
The organization
&lt;a href="#the-organization" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>PSPDFKit is a distributed company where everyone works remotely. The PSPDFKit Server team includes people with overlapping time zones and no fixed working hours.&lt;/p>
&lt;p>The core business for the company is the sale of SDKs used to manipulate documents (predominantly in PDF format).&lt;/p>
&lt;p>SDKs support a variety of platforms (iOS, Android, Windows, and web) and are embedded in client applications written and maintained by our customers.&lt;/p>
&lt;h2 id="the-project">
The Project
&lt;a href="#the-project" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>PSPDFKit Server’s codebase is more than five years old and has grown organically over time thanks to the work of people with different expertise and backgrounds.&lt;/p>
&lt;p>Domain-wise, the application manages the lifetime of a document, allowing our customers to build specific workflows on top of a small set of primitives: documents, content layers, annotations of different types, and permission sets. At a very high level, a user can open a document, perform an arbitrary set of actions (with the option of leveraging real-time collaboration with other users), and finish their session with changes automatically and efficiently stored.&lt;/p>
&lt;p>Due to the way the product is sold and operated, it has a few key properties that make it different from most projects I’ve ever worked on:&lt;/p>
&lt;ol>
&lt;li>It’s both CPU and memory intensive due to the amount of operations performed on PDF files. The component that interacts with PDF files is run as a binary daemon and maintained by a different team.&lt;/li>
&lt;li>The application has optional components that are activated depending on specific configuration options. For example, one can enable an entire caching layer based on Redis.&lt;/li>
&lt;li>Features can be turned on and off remotely via our licensing infrastructure.&lt;/li>
&lt;li>The release is shipped as a Docker image to be operated on premise by the customer. This property has two implications. First, it means that we don&amp;rsquo;t have direct access to performance monitoring or visibility over runtime issues. Second, it drives us to minimise the operational complexity of the end product; for example, we don&amp;rsquo;t leverage the native Erlang/Elixir distribution because that would complicate deployment for our customers.&lt;/li>
&lt;/ol>
&lt;h2 id="onboarding-results">
Onboarding results
&lt;a href="#onboarding-results" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>In the last two years, we managed to onboard a good number of people:&lt;/p>
&lt;ul>
&lt;li>1 external hire with extensive Elixir experience, now part of the team full time&lt;/li>
&lt;li>1 internal hire with no Elixir experience, now part of the team full time&lt;/li>
&lt;li>2 internal hires with no Elixir experience, in the process of joining the team full time&lt;/li>
&lt;li>1 person contributing a significant piece of work over a few weeks&lt;/li>
&lt;li>Occasional contributions by other people&lt;/li>
&lt;/ul>
&lt;p>It’s great that we can facilitate different degrees of contribution, with a ramp-up time measure between a couple of days and a week.&lt;/p>
&lt;p>The people involved have completely different backgrounds: heavy frontend development, systems programming, and mobile development. Furthermore, apart from one person, everyone had very little functional programming experience.&lt;/p>
&lt;h2 id="training-structure">
Training structure
&lt;a href="#training-structure" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;h3 id="learning-styles">
Learning styles
&lt;a href="#learning-styles" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>An important aspect to consider is each person’s learning style. Some people prefer pair-programming, while others would rather go through tutorials in their own time and ask questions when needed.&lt;/p>
&lt;p>To accommodate such differences, we combine learning materials with targeted pairing sessions as well as follow-up calls to clarify where needed. We also keep the schedule pretty flexible, meaning that we don&amp;rsquo;t have a fixed curriculum people go through in a set period of time.&lt;/p>
&lt;h3 id="tooling">
Tooling
&lt;a href="#tooling" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>The company uses a diverse set of languages and associated development tools. This ranges from text editors to full-fledged IDEs, as well as running on MacOS, Windows, and Linux.&lt;/p>
&lt;p>Apart from work on platforms where there’s strong incentive to standardize on a specific tool (e.g. XCode for iOS development), people pick their tools based on personal preference.&lt;/p>
&lt;p>A quick informal survey of the Server Team reveals that people predominantly use MacOS and favor Vim, Emacs, VSCode, and JetBrains IntelliJ.&lt;/p>
&lt;p>We enforce code formatting at the CI level, but encourage people to set up their development environment to format on file save.&lt;/p>
&lt;p>For code intelligence, Elixir has a solid implementation of a &lt;a href="https://github.com/elixir-lsp/elixir-ls">Language Server&lt;/a> that provides intelligent autocompletion, inline docs, and jump to definition. Especially for people coming from a Java background, this type of tooling helps provide a consistent experience.&lt;/p>
&lt;p>In terms of domain-specific tooling, we glue everything together with shell scripts and Docker-based workflows.&lt;/p>
&lt;h3 id="productivity-ramp-up-time">
Productivity ramp-up time
&lt;a href="#productivity-ramp-up-time" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>People are usually productive in two to three days, where &amp;ldquo;being productive&amp;rdquo; means being able to set up the project, getting a basic understanding of its structure and architecture, taking on a small piece of work, and producing a relevant pull request containing implementation, tests, and documentation.&lt;/p>
&lt;p>You may have heard the mantra &amp;ldquo;Make a commit on your first day.&amp;rdquo; While I agree with the sentiment behind it, I find it puts too much pressure on the contributor.&lt;/p>
&lt;p>As part of the initial onboarding, it&amp;rsquo;s normal to find little issues with setup scripts (e.g. things that stop working between major OS versions) or unclear documentation. In most cases, fixes are cheap to implement and offer an opportunity to discuss the application architecture.&lt;/p>
&lt;h3 id="proficiency">
Proficiency
&lt;a href="#proficiency" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>On average, it takes two to three months for an engineer to become a proficient contributor who has worked on most areas of the codebase, including some of the internal parts which are subject to very low churn over time.&lt;/p>
&lt;h3 id="learning-materials">
Learning materials
&lt;a href="#learning-materials" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>We generally recommend going through the &lt;a href="https://elixir-lang.org/getting-started/introduction.html">Elixir Lang guides&lt;/a>: they’re well written, accurate, and split in easy-to-digest chunks.&lt;/p>
&lt;p>For all libraries, making package documentation available on &lt;a href="https://hexdocs.pm">hexdocs&lt;/a> tends to be sufficient.&lt;/p>
&lt;p>While we do have a few books available in the company digital library, they&amp;rsquo;re rarely consulted.&lt;/p>
&lt;h3 id="scope">
Scope
&lt;a href="#scope" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>We start with small, incremental features, as they have a very high completion success rate. For internal hires, we try to choose something connected to a use case the person is already familiar with from previous work.&lt;/p>
&lt;p>A more experienced team member is available for general guidance, so that the resulting piece of work includes all necessary components: implementation, tests, and documentation.&lt;/p>
&lt;p>On a technical level, we try to avoid pieces of work that introduce more than one key concept. For example, if the person is not familiar with immutable data structures, we avoid assigning a feature that would also introduce concurrency patterns.&lt;/p>
&lt;h3 id="example-projects">
Example projects
&lt;a href="#example-projects" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>There are cases where it’s worth starting from smaller, separate projects. For concurrency and distribution, it’s better to rely on smaller applications (e.g. the examples referenced in the Elixir Lang guides) to nail the basics.&lt;/p>
&lt;p>Once the basics are clear, we build up by looking at codebase components to address topics like error handling, logging, production hardening, and recovery.&lt;/p>
&lt;p>Learning is complemented by specific articles (e.g. an introduction to circuit breakers).&lt;/p>
&lt;h3 id="providing-feedback">
Providing feedback
&lt;a href="#providing-feedback" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>When a person starts to submit code for review, we try to provide different layers of feedback:&lt;/p>
&lt;ul>
&lt;li>At first, we focus only on functionality, so that things work as expected. At this stage, the purpose is to build confidence and get the person comfortable.&lt;/li>
&lt;li>We address code quality and language conventions in follow-up PRs, so that changes can be reviewed separately and it’s clearer why they’re necessary.&lt;/li>
&lt;li>Reviews include links for self-learning, so that the person can explore on their own.&lt;/li>
&lt;li>If a PR review becomes too long, we suggest a pairing session to discuss things directly.&lt;/li>
&lt;li>We try to use proper names for all concepts, but also be conservative about introducing too many ideas in the same feedback session.&lt;/li>
&lt;li>We make a clear distinction between company and community conventions, as they may differ. This way, when the developer reads other code on their own, they can compare and contrast approaches. This also often provides constructive criticism on the way we do things.&lt;/li>
&lt;li>We slow down as needed during pairing sessions focused on learning. We allow ourselves to diverge from the initial session goal if the person wants to drill into a different topic. Maintaining interest and enthusiasm is more important in the early stages than sticking to a predefined plan.&lt;/li>
&lt;li>We recently started recording short videos that document how to approach specific development tasks in order to facilitate independent learning.&lt;/li>
&lt;/ul>
&lt;h3 id="milestones">
Milestones
&lt;a href="#milestones" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>Every person that learned Elixir in the company followed a different process, but it’s possible to identify common milestones.&lt;/p>
&lt;h4 id="pattern-matching-and-control-flow">
Pattern matching and control flow
&lt;a href="#pattern-matching-and-control-flow" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>Understanding pattern-matching comes in the first hour of reading existing code. It takes more time to develop a sensibility around specifics, such as when to use a single function head with an internal case statement versus multiple function heads with specific pattern matching clauses.&lt;/p>
&lt;h4 id="immutable-data-and-functions">
Immutable data and functions
&lt;a href="#immutable-data-and-functions" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>For people who approach Elixir as their first functional language coming from object-oriented languages, the first adaptation step is to start reasoning in terms of data passed through functions.&lt;/p>
&lt;p>Our trainees quickly embraced immutability due perceived benefits in clarity and predictability of the program. For example, one of our engineers commented that at the beginning of his learning journey, he could sprinkle print statements all over the codebase to see values changing and visualize the application flow.&lt;/p>
&lt;p>In this phase, developers tended to write very explicit code, with plenty of intermediate bindings and little use of more advanced language features like the &lt;code>|&amp;gt;&lt;/code> operator or the &lt;code>with&lt;/code> special form.&lt;/p>
&lt;h4 id="configuration">
Configuration
&lt;a href="#configuration" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>Configuration usually triggers a few questions:&lt;/p>
&lt;ul>
&lt;li>Should this value be configurable?&lt;/li>
&lt;li>What about changing the value at runtime?&lt;/li>
&lt;li>How does configuration interact with building a Docker image and, later on, the container runtime?&lt;/li>
&lt;/ul>
&lt;p>This is usually a good opportunity to introduce compile-time vs. runtime configuration, along with the operational implications of relying on the &lt;code>Application.*&lt;/code> functions to get and set configuration values at runtime.&lt;/p>
&lt;p>Common issues include hitting some light race conditions, namely unexpected test results due to configuration values being changed non-deterministically. It&amp;rsquo;s a great opportunity to reinforce that we should strive to write deterministic code that reads configuration as early as possible and passes relevant values down the line.&lt;/p>
&lt;h4 id="concurrency-and-otp">
Concurrency and OTP
&lt;a href="#concurrency-and-otp" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>The application has a few concurrent components, but their structure is rarely modified and developers can use public APIs without much concern for the internals.&lt;/p>
&lt;p>This is a big advantage and it lets us plan when to look at the concurrency model. We normally suggest looking at the examples in the Elixir Lang guides, then scheduling a couple of pairing sessions to look at the most significant components included in our codebase.&lt;/p>
&lt;p>Once we&amp;rsquo;re done with the fundamentals, we approach concurrency patterns: pools, worker queues, tasks, etc. For such cases, we train only on our application codebase.&lt;/p>
&lt;h4 id="state-management">
State management
&lt;a href="#state-management" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>The application depends on a few stateful components: metadata store, assets store, and different layers of caching (both filesystem and memory).&lt;/p>
&lt;p>These components provide the opportunity to introduce ETS tables, process lifetimes and recovery patterns, OTP guarantees around process trees, and error-handling logic.&lt;/p>
&lt;h4 id="metaprogramming">
Metaprogramming
&lt;a href="#metaprogramming" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>We found very little use for meta-programming, so we don&amp;rsquo;t make it an explicit part of the onboarding process. Most of the time, people look into it by themselves when they start exploring the internals of dependencies like Phoenix and Ecto.&lt;/p>
&lt;h3 id="bumps-on-the-road">
Bumps on the road
&lt;a href="#bumps-on-the-road" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h3>
&lt;p>What follows is a list of road bumps we encountered over time. Some of them are more tied to our application and the way we ship it, so they don&amp;rsquo;t necessarily represent issues with the language or the platform.&lt;/p>
&lt;h4 id="dialyzer">
Dialyzer
&lt;a href="#dialyzer" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>Especially for people with knowledge of programming languages with static types, having Dialyzer as a separate step from the compiler can be frustrating.&lt;/p>
&lt;p>Fortunately, the aforementioned Language Server and related projects make it possible to set up the editing environment to provide almost real-time type information with a reasonable level of accuracy.&lt;/p>
&lt;h4 id="structuring-large-components">
Structuring large components
&lt;a href="#structuring-large-components" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>One common observation is that compared to other languages, Elixir applications are built on very few architectural patterns. This applies to both ours and others I&amp;rsquo;ve worked on in the past few years, and is especially noticeable outside concurrency-related areas (which very often implement explicit behaviours).&lt;/p>
&lt;p>Most of the time, we simply take care of separating modules that perform side effects (e.g. writing/reading files, http calls, database interaction) from modules which only manipulate data.&lt;/p>
&lt;p>Modules are grouped according to their main &amp;ldquo;topic,&amp;rdquo; which helps exploration.&lt;/p>
&lt;p>Having fewer patterns sometimes makes the conversation slightly more difficult, as you need to develop a shared, domain-specific vocabulary to quickly express more complex ideas. On the plus side, the effort of defining names helps onboarding immensely, because it forces everyone in the team to be precise and thorough in their explanations.&lt;/p>
&lt;h4 id="error-handling-and-recovery">
Error handling and recovery
&lt;a href="#error-handling-and-recovery" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h4>
&lt;p>As we don&amp;rsquo;t operate the software we write, we need to take extra care with error handling. As best we can, we want to provide error messages that are simple(r) to understand for our customers (who are not Elixir developers).&lt;/p>
&lt;p>This creates a very mild tension between our style of error handling and the literature and community examples around applications being run directly by the developers who write them.&lt;/p>
&lt;p>For example, it&amp;rsquo;s much better for us to produce a single line error log with a specific name we understand compared to a more verbose stack trace, because it facilitates communication with the customer.&lt;/p>
&lt;h2 id="conclusion">
Conclusion
&lt;a href="#conclusion" class="h-anchor" aria-hidden="true">#&lt;/a>
&lt;/h2>
&lt;p>I often hear the argument, &amp;ldquo;it&amp;rsquo;s difficult to hire Elixir developers.&amp;rdquo; In my experience, once you have two reasonably knowledgeable engineers, you can stop searching for already-trained developers and expand the search to developers willing to learn the language. With some care around the onboarding process, it&amp;rsquo;s possible to successfully train a new developer every two to three months with a very reasonable impact on the rest of the team.&lt;/p>
&lt;p>&lt;em>Thanks to &lt;a href="mailto:swatkinsyl@gmail.com">Susan Watkins&lt;/a> for editing this article&lt;/em>&lt;/p></description></item><item><title>The Perils of Large Files</title><link>https://claudio-ortolina.org/posts/the-perils-of-large-files/</link><pubDate>Mon, 05 Apr 2021 11:00:00 +0000</pubDate><guid>https://claudio-ortolina.org/posts/the-perils-of-large-files/</guid><description>
&lt;p>
I recently wrote a blog post about working with large files in Elixir and OTP at &lt;a href="https://pspdfkit.com/blog/2021/the-perils-of-large-files-in-elixir/">PSPDFKit&amp;#39;s blog&lt;/a>.&lt;/p>
&lt;p>
The post originated from a talk I gave last year for &lt;a href="https://www.elixirconf.eu">ElixirConf.eu&lt;/a>, but it contains more precise analysis.&lt;/p></description></item><item><title>The Race for Space</title><link>https://claudio-ortolina.org/posts/the-race-for-space/</link><pubDate>Sat, 28 Nov 2020 17:55:37 +0000</pubDate><guid>https://claudio-ortolina.org/posts/the-race-for-space/</guid><description>
&lt;img src="https://claudio-ortolina.org/img/the-race-for-space/cover.jpg"/>
&lt;p>
&lt;a href="http://publicservicebroadcasting.net/">Public Service Broadcasting&lt;/a> (PBS for short) are a trio based in London, whose music usually involves historical audio documents with a mix of jazz, rock and electronic.&lt;/p>
&lt;p>
Each one of their album revolves around a specific theme, usually inspired by modern/contemporary history, and includes a vast amount of archival audio footage, recording, and samples, with very little vocals on top.&lt;/p>
&lt;p>
In 2015 they released &lt;a href="https://en.wikipedia.org/wiki/The_Race_for_Space_(album)">The Race for Space&lt;/a>, a concept album that explores the first 15 years of the &lt;a href="https://en.wikipedia.org/wiki/Space_Race">Space Race&lt;/a> missions, from &lt;a href="https://en.wikipedia.org/wiki/Sputnik_1">Sputnik 1&lt;/a>&amp;#39;s flight in 1957 to the &lt;a href="https://en.wikipedia.org/wiki/Apollo_17">last Apollo mission&lt;/a> in 1972.&lt;/p>
&lt;p>
The general tone celebrates achievements of both US and Soviet Union, emphasising each step as a human endeavour that transcends borders and politics.&lt;/p>
&lt;p>
Between music, vocals and engineering, the album involved another 35 people, which is something you can only perceive after listening to it a few times: some of the instruments are particularly subtle and deserve special attention.&lt;/p>
&lt;p>
Each song has a different personality and it&amp;#39;s difficult to attribute a general genre to the entire album, but I can feel a general underlying cohesion which makes it that I rarely listen to one song and prefer to just play the entire record.&lt;/p>
&lt;div id="outline-container-headline-1" class="outline-2">
&lt;h2 id="headline-1">
1. The Race for Space
&lt;/h2>
&lt;div id="outline-text-headline-1" class="outline-text-2">
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/VlnwuV6RuMo?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
The album opens with rearranged fragments of &lt;a href="https://en.wikipedia.org/wiki/We_choose_to_go_to_the_Moon">US President John F. Kennedy&amp;#39;s speech&lt;/a> given at Rice University, Houston, Texas, on September 12, 1962. The speech sets the tone of the entire album, celebrating mankind&amp;#39;s efforts to reach beyond the stars:&lt;/p>
&lt;blockquote>
&lt;p>Many years ago, Great British explorer George Mallory &lt;br>
Who was to die on Mount Everest &lt;br>
Was asked &amp;#39;why did he want to climb it?&amp;#39; &lt;br>
He said &amp;#39;because it is there&amp;#39; &lt;br>
Well space is there and we&amp;#39;re going to climb it &lt;br>
And the moon and the planets are there &lt;br>
And new hopes for knowledge and peace are there &lt;br>
And therefore as we set sail we ask God&amp;#39;s blessing &lt;br>
On the most hazardous, and dangerous, and greatest adventure &lt;br>
On which man has ever embarked – John F. Kennedy&lt;/p>
&lt;/blockquote>
&lt;p>
It&amp;#39;s difficult to listen to this speech without anticipation, especially knowing &lt;a href="https://en.wikipedia.org/wiki/Apollo_11">what would happen 7 years later&lt;/a>.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-2" class="outline-2">
&lt;h2 id="headline-2">
2. Sputnik
&lt;/h2>
&lt;div id="outline-text-headline-2" class="outline-text-2">
&lt;p>
The second track, &lt;em>Sputnik&lt;/em>, takes us to the Soviet Union in 1957, to tell the story of the first object ever sent into space, the low Earth orbit satellite &lt;a href="https://en.wikipedia.org/wiki/Sputnik_1">Sputnik 1&lt;/a>.&lt;/p>
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/It42TsD7_sI?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
In &lt;em>Sputnik&lt;/em>, the original sounds of the unmanned spacecraft gradually become part of the main beat and gradually build excitement for the beginning of &amp;#34;man&amp;#39;s cosmic existence&amp;#34;.&lt;/p>
&lt;blockquote>
&lt;p>The man made celestial body, for the first time in history &lt;br>
Overcame terrestrial gravity and flew into space &lt;br>
All men of all nations recognise this as a great achievement &lt;br>
In an age where the race to conquer space has become an all-absorbing factor&lt;/p>
&lt;p>
The era of man&amp;#39;s cosmic existence&lt;/p>
&lt;/blockquote>
&lt;p>
Once again, the accent is on how people can be united by such an important event:&lt;/p>
&lt;blockquote>
&lt;p>All over the world, people are tuning in to the &amp;#39;bleep bleep bleep&amp;#39; of the satellite&lt;/p>
&lt;p>
A dream cherished by men for many centuries comes true on October the 4th, 1957&lt;/p>
&lt;/blockquote>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-3" class="outline-2">
&lt;h2 id="headline-3">
3. Gagarin
&lt;/h2>
&lt;div id="outline-text-headline-3" class="outline-text-2">
&lt;p>
If &lt;em>Sputnik&lt;/em> makes you tap your feet, then &lt;em>Gagarin&lt;/em> will make you dance: to tell the story of the first ever cosmonaut, &lt;a href="https://en.wikipedia.org/wiki/Yuri_Gagarin">Yuri Gagarin&lt;/a>, PBS wrote an explosive song, a mix of funk, brass, marching band, you-name-it tune that captures the euphoria of such an important moment.&lt;/p>
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/wY-kAnvOY80?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
The jump into the unknown is not something to be scared of:&lt;/p>
&lt;blockquote>
&lt;p>The World&amp;#39;s first cosmonaut &lt;br>
The first to open the door into the unknown &lt;br>
The first to step over the threshold of our homeland&lt;/p>
&lt;/blockquote>
&lt;p>
Gagarin himself appreciated his new vantage point:&lt;/p>
&lt;blockquote>
&lt;p>Astronaut to Earth: I can see forests, rivers, [?] all around &lt;br>
Everything&amp;#39;s so beautiful, it&amp;#39;s wonderful, it wonderful…&lt;/p>
&lt;/blockquote>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-4" class="outline-2">
&lt;h2 id="headline-4">
4. Fire in the Cockpit
&lt;/h2>
&lt;div id="outline-text-headline-4" class="outline-text-2">
&lt;p>
After &lt;em>Gagarin&lt;/em>, &lt;em>Fire in the Cockpit&lt;/em> is a cold shower and a stark reminder of the risks and cost of exploration, telling the tragedy of &lt;a href="https://en.wikipedia.org/wiki/Apollo_1">Apollo 1&lt;/a>, where three astronauts lost their lives due to a cabin fire during a test.&lt;/p>
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/QLA9-1U7Vrw?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
The entire song revolves around the sound of static and deep keyboard tones, with a cello entering midway as we listen to the words of NASA&amp;#39;s description of the events. It&amp;#39;s a sad song, but exhibits the composure and respect owed to people who lost their lives while trying to advance the frontiers of human knowledge.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-5" class="outline-2">
&lt;h2 id="headline-5">
5. E.V.A.
&lt;/h2>
&lt;div id="outline-text-headline-5" class="outline-text-2">
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/PFSq4Q8WDs0?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
In &lt;em>E.V.A.&lt;/em>&lt;sup class="footnote-reference">&lt;a id="footnote-reference-1" href="#footnote-1">1&lt;/a>&lt;/sup> we hear the story of Alexei Leonov, Alexei Leonov completed the first spacewalk in 1965, spending 10 minutes outside in open space.&lt;/p>
&lt;p>
It&amp;#39;s interesting how the music almost stops when the astronaut leaves the spacecraft, mimicking the silence of the vacuum of space. The few piano notes we can hear really complement the marvel of Leonov&amp;#39;s words:&lt;/p>
&lt;blockquote>
&lt;p>I&amp;#39;m on the edge of the opening &lt;br>
Of the airlock chamber &lt;br>
I feel excellent &lt;br>
I see clouds and the sea &lt;br>
I am beginning to move away&lt;/p>
&lt;/blockquote>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-6" class="outline-2">
&lt;h2 id="headline-6">
6. The Other Side
&lt;/h2>
&lt;div id="outline-text-headline-6" class="outline-text-2">
&lt;p>
&lt;em>The Other Side&lt;/em> takes us one step closer to the moon landing, focusing on the &lt;a href="https://en.wikipedia.org/wiki/Apollo_8">Apollo 8&lt;/a> mission, where for the first time a manned spacecraft completed an orbit of the moon, but from the unusual point of view of ground control.&lt;/p>
&lt;p>
We hear voices and recordings from the control room, where ground control monitors the spacecraft as it&amp;#39;s about to reach the blind side of the moon. The excitement and anxiety are palpable: to complete a lunar orbit, Apollo will temporarily lose signal with earth.&lt;/p>
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/P8LlUrT7MFo?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
Once again, the music tells the story without words, getting quiet during loss of signal and exploding into a liberating instrumental when Apollo finally replies back to Houston. The event is incredibly significant:&lt;/p>
&lt;blockquote>
&lt;p>The unmanned lunar spacecraft traversed the moon perhaps over 10, 000 times &lt;br>
But this is the first that a man aboard reported to his compatriots here on Earth&lt;/p>
&lt;/blockquote>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-7" class="outline-2">
&lt;h2 id="headline-7">
7. Valentina
&lt;/h2>
&lt;div id="outline-text-headline-7" class="outline-text-2">
&lt;p>
&lt;em>Valentina&lt;/em> is a celebration of &lt;a href="https://en.wikipedia.org/wiki/Valentina_Tereshkova">Valentina Tereshkova&lt;/a>, the first woman to ever go to space n 1963 (and to this date, the only one having ever been in a solo mission).&lt;/p>
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/Bnmq4WR83Mw?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
The song, which features choruses from &lt;a href="http://www.smokefairies.com/">The Smoke Fairies&lt;/a>, is a graceful instrumental without any other vocals. J. Willgoose, Esq. writes on the matter:&lt;/p>
&lt;blockquote>
&lt;p>One of the biggest problems with the material we use, from the period we address, is that it almost totally devoid of any female voice. &lt;br>
It is often said that history is written by the winners, but it would be equally if not more apt to say that it has overwhelmingly been written by men. Of the footage I obtained of the first woman in space, all of it featured her voice being translated by male voices. &lt;br>
Rather than yet more men - us, in this case - attempting to speak on her behalf, it seemed more appropriate to ask a guest singer to provide a female voice, so we tried a different approach with &amp;#39;Valentina&amp;#39; and I&amp;#39;m very glad we did.&lt;/p>
&lt;/blockquote>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-8" class="outline-2">
&lt;h2 id="headline-8">
8. Go!
&lt;/h2>
&lt;div id="outline-text-headline-8" class="outline-text-2">
&lt;p>
The story of the &lt;a href="https://en.wikipedia.org/wiki/Apollo_11">Apollo 11&lt;/a> and the first crew to land on the moon in 1969 represents one of the most important moments of human history. Once again, PBS decides to focus on the point of view of the people on the ground, to celebrate the often unseen work of preparation, monitoring, incredible engineering that made the whole thing possible.&lt;/p>
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/BHIo6qwJarI?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
&lt;em>Go!&lt;/em> is giant checklist, where we hear the flight director Gene Kranz go through all the checks needed to make sure that the descent on the moon will be successful. It&amp;#39;s so interesting that the landing itself is just a couple of verses in the middle:&lt;/p>
&lt;blockquote>
&lt;p>Houston, uh &lt;br>
Tranquility base here, The Eagle has landed&lt;/p>
&lt;/blockquote>
&lt;p>
The repetition in the dialogues provides the rhythm of the song, which matches the excitement of the mission with upbeat percussions, synth and keyboard.&lt;/p>
&lt;p>
&lt;em>Go!&lt;/em> is a reminder that we can achieve the impossible if we work together.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-9" class="outline-2">
&lt;h2 id="headline-9">
9. Tomorrow
&lt;/h2>
&lt;div id="outline-text-headline-9" class="outline-text-2">
&lt;p>
The &lt;a href="https://en.wikipedia.org/wiki/Apollo_17">Apollo 17&lt;/a> mission, the last in the Apollo program, represents the end of an era and the last time we landed on the moon.&lt;/p>
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/5Id8P6yvcWs?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
&lt;em>Tomorrow&lt;/em> reflects on its significance: as a species, we managed to leave our own planet, albeit temporarily, and look beyond to a completely unexplored universe.&lt;/p>
&lt;p>
While it&amp;#39;s not possible to separate the space race from the politics that fueled it in the first place, it&amp;#39;s also a testament to the effort of thousands of people over decades, to literally take us where no one has ever been before.&lt;/p>
&lt;p>
As an outro, &lt;em>Tomorrow&lt;/em> tempers the excitement of the previous songs and focuses more on choruses and keyboard, painting a picture of anticipation of what&amp;#39;s gonna come next.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div class="footnotes">
&lt;hr class="footnotes-separatator">
&lt;div class="footnote-definitions">
&lt;div class="footnote-definition">
&lt;sup id="footnote-1">&lt;a href="#footnote-reference-1">1&lt;/a>&lt;/sup>
&lt;div class="footnote-body">
&lt;p>the name is a shorthand for &lt;em>extravehicular activity&lt;/em>, which indicates a spacesuit designed for usage outside of a vehicle.&lt;/p>
&lt;/div>
&lt;/div>
&lt;/div>
&lt;/div></description></item><item><title>Building a Custom Page for Phoenix Live Dashboard</title><link>https://claudio-ortolina.org/posts/building-a-custom-page-for-phoenix-live-dashboard/</link><pubDate>Sat, 21 Nov 2020 09:29:01 +0000</pubDate><guid>https://claudio-ortolina.org/posts/building-a-custom-page-for-phoenix-live-dashboard/</guid><description>
&lt;p>
One of the most interesting features provided by Phoenix Live Dashboard is the ability to &lt;a href="https://hexdocs.pm/phoenix_live_dashboard/Phoenix.LiveDashboard.PageBuilder.html#content">define your own pages&lt;/a>, so that you can quickly and reliably extend a Live Dashboard instance with sections that are tailored to your application domain.&lt;/p>
&lt;p>
While working on &lt;a href="https://github.com/fully-forged/tune">Tune&lt;/a>, I found a use case suitable for a custom live dashboard page: a debugging view where I can check open sessions and inspect the underlying processes.&lt;/p>
&lt;div id="outline-container-headline-1" class="outline-2">
&lt;h2 id="headline-1">
On the use case
&lt;/h2>
&lt;div id="outline-text-headline-1" class="outline-text-2">
&lt;p>
I would encourage you to read &lt;a href="https://github.com/fully-forged/tune">Tune&amp;#39;s README&lt;/a> to understand the use case in more detail, but I&amp;#39;ll quote the relevant architectural section:&lt;/p>
&lt;blockquote>
&lt;p>Tune assumes multiple browser sessions for the same user, which is why it defines a &lt;a href="https://tune-docs.fullyforged.com/Tune.Spotify.Session.html#content">&lt;code>Tune.Spotify.Session&lt;/code>&lt;/a> behaviour with &lt;a href="https://tune-docs.fullyforged.com/Tune.Spotify.Session.HTTP.html#content">&lt;code>Tune.Spotify.Session.HTTP&lt;/code>&lt;/a> as its main runtime implementation.&lt;/p>
&lt;p>
Each worker is responsible to proxy interaction with the Spotify API, periodically poll for data changes, and broadcast corresponding events.&lt;/p>
&lt;p>
When a user opens a browser session, &lt;a href="https://tune-docs.fullyforged.com/TuneWeb.ExplorerLive.html#content">&lt;code>TuneWeb.ExplorerLive&lt;/code>&lt;/a> either starts or simply reuses a worker named with the same session ID.&lt;/p>
&lt;p>
Each worker monitors its subscribers, so that it can shutdown when a user closes their last browser window.&lt;/p>
&lt;p>
This architecture ensures that:&lt;/p>
&lt;ul>
&lt;li>The amount of automatic API calls against the Spotify API for a given user is constant and independent from the number of user sessions for the same user.&lt;/li>
&lt;li>Credential renewal happens in the background&lt;/li>
&lt;li>The explorer implementation remains entirely focused on UI interaction&lt;/li>
&lt;/ul>
&lt;/blockquote>
&lt;p>
In other words:&lt;/p>
&lt;ul>
&lt;li>For each Spotify account connected, there can only be one session (a &lt;code>Tune.Spotify.Session.HTTP&lt;/code> process named with the session ID).&lt;/li>
&lt;li>For each session, there can be many open clients (i.e. browser windows or &lt;code>TuneWeb.ExplorerLive&lt;/code> process).&lt;/li>
&lt;/ul>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-2" class="outline-2">
&lt;h2 id="headline-2">
Requirements
&lt;/h2>
&lt;div id="outline-text-headline-2" class="outline-text-2">
&lt;p>
Our dashboard page will include:&lt;/p>
&lt;ul>
&lt;li>A table with session IDs, PIDs and count of open clients&lt;/li>
&lt;li>Ability to sort by session ID or clients count&lt;/li>
&lt;li>Search by session ID&lt;/li>
&lt;li>Support multiple nodes&lt;/li>
&lt;/ul>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-3" class="outline-2">
&lt;h2 id="headline-3">
Gathering the necessary data
&lt;/h2>
&lt;div id="outline-text-headline-3" class="outline-text-2">
&lt;p>
To populate the dashboard table, we first need to find a way to get a list of all active sessions, along with their clients count.&lt;/p>
&lt;p>
The simplest way is to leverage the fact that each &lt;code>Tune.Spotify.Session.HTTP&lt;/code> process is started with a name managed via a &lt;a href="https://hexdocs.pm/elixir/Registry.html">Registry&lt;/a>, with the session ID as a key. Registration is in place to guarantee that there can only be one session process with the same ID on each node.&lt;/p>
&lt;p>
We can use &lt;a href="https://hexdocs.pm/elixir/Registry.html#select/2">&lt;code>Registry.select/2&lt;/code>&lt;/a> to query the registry and receive back all session IDs and PIDs:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">Registry&lt;/span>&lt;span style="color:#f92672">.&lt;/span>select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Tune.Spotify.SessionRegistry&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [{{&lt;span style="color:#e6db74">:&amp;#34;$1&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">:&amp;#34;$2&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">:_&lt;/span>}, [], [{{&lt;span style="color:#e6db74">:&amp;#34;$1&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">:&amp;#34;$2&amp;#34;&lt;/span>}}]}]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
Which returns:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>[{&lt;span style="color:#e6db74">&amp;#34;claudio.ortolina&amp;#34;&lt;/span>, &lt;span style="color:#75715e">#PID&amp;lt;0.565.0&amp;gt;}]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
In the example above, we use a match specification to capture the registry key (the session ID) and the registered PID.&lt;/p>
&lt;p>
It&amp;#39;s important to understand straight away the constraints associated with this approach:&lt;/p>
&lt;ul>
&lt;li>A &lt;code>Registry&lt;/code> is normally split into a variable number of partitions, so this query has to visit all partitions to return its results. While this is not a problem at this stage (the application has very little load), it can become a bottleneck once the number of registered processes grows.&lt;/li>
&lt;li>As data is partitioned, it&amp;#39;s not possible to apply sort order or limit the results without concatenating them all first, which means that both operations will need to be done by the caller.&lt;/li>
&lt;li>Results only apply to the current node, which works well with Phoenix Live Dashboard&amp;#39;s general structure, which always operates on one node at a time.&lt;/li>
&lt;/ul>
&lt;p>Given the registry query above, we can implement a function that provides the data necessary to populate an unfiltered, unsorted version of the table:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">defmodule&lt;/span> &lt;span style="color:#a6e22e">Tune.Spotify.Supervisor&lt;/span> &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># omitted&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> sessions &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Tune.Spotify.SessionRegistry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">|&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Registry&lt;/span>&lt;span style="color:#f92672">.&lt;/span>select([{{&lt;span style="color:#e6db74">:&amp;#34;$1&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">:&amp;#34;$2&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">:_&lt;/span>}, [], [{{&lt;span style="color:#e6db74">:&amp;#34;$1&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">:&amp;#34;$2&amp;#34;&lt;/span>}}]}])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">|&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Enum&lt;/span>&lt;span style="color:#f92672">.&lt;/span>map(&lt;span style="color:#66d9ef">fn&lt;/span> {id, pid} &lt;span style="color:#f92672">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subscribers_count &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">Tune.Spotify.Session.HTTP&lt;/span>&lt;span style="color:#f92672">.&lt;/span>subscribers_count(id)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> %{&lt;span style="color:#e6db74">id&lt;/span>: id, &lt;span style="color:#e6db74">pid&lt;/span>: pid, &lt;span style="color:#e6db74">clients_count&lt;/span>: subscribers_count}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
The resulting data structure is a map with the necessary data:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>[%{&lt;span style="color:#e6db74">clients_count&lt;/span>: &lt;span style="color:#ae81ff">1&lt;/span>, &lt;span style="color:#e6db74">id&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;claudio.ortolina&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">pid&lt;/span>: &lt;span style="color:#75715e">#PID&amp;lt;0.565.0&amp;gt;}]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-4" class="outline-2">
&lt;h2 id="headline-4">
Dashboard page structure
&lt;/h2>
&lt;div id="outline-text-headline-4" class="outline-text-2">
&lt;p>
To build a dashboard page, we need to:&lt;/p>
&lt;ol>
&lt;li>Create a module that implements the &lt;code>use Phoenix.LiveDashboard.PageBuilder&lt;/code> behaviour.&lt;/li>
&lt;li>Mount that module into the Live Dashboard configuration defined into our application router.&lt;/li>
&lt;/ol>
&lt;p>What follows is a minimal implementation that shows the data we need, with the following limitations:&lt;/p>
&lt;ul>
&lt;li>no searching, sorting or limiting capabilities&lt;/li>
&lt;li>works only on a single node&lt;/li>
&lt;/ul>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">defmodule&lt;/span> &lt;span style="color:#a6e22e">TuneWeb.LiveDashboard.SpotifySessionsPage&lt;/span> &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@moduledoc&lt;/span> &lt;span style="color:#66d9ef">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">use&lt;/span> &lt;span style="color:#a6e22e">Phoenix.LiveDashboard.PageBuilder&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@impl&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> menu_link(_, _) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#e6db74">:ok&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;Spotify Sessions&amp;#34;&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@impl&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> render_page(_assigns) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> table(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">columns&lt;/span>: columns(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">id&lt;/span>: &lt;span style="color:#e6db74">:spotify_sessions&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">row_attrs&lt;/span>: &lt;span style="color:#f92672">&amp;amp;&lt;/span>row_attrs&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">row_fetcher&lt;/span>: &lt;span style="color:#f92672">&amp;amp;&lt;/span>fetch_sessions&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">2&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">rows_name&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;sessions&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">title&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Spotify Sessions&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defp&lt;/span> fetch_sessions(_params, _node) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># returns [%{clients_count: 1, id: &amp;#34;claudio.ortolina&amp;#34;, pid: #PID&amp;lt;0.565.0&amp;gt;}]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sessions &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">Tune.Spotify.Supervisor&lt;/span>&lt;span style="color:#f92672">.&lt;/span>sessions()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {sessions, length(sessions)}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defp&lt;/span> columns &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> %{&lt;span style="color:#e6db74">field&lt;/span>: &lt;span style="color:#e6db74">:id&lt;/span>, &lt;span style="color:#e6db74">header&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Session ID&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">sortable&lt;/span>: &lt;span style="color:#e6db74">:asc&lt;/span>},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> %{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">field&lt;/span>: &lt;span style="color:#e6db74">:pid&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">header&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Worker PID&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">format&lt;/span>: &lt;span style="color:#f92672">&amp;amp;&lt;/span>(&amp;amp;1 &lt;span style="color:#f92672">|&amp;gt;&lt;/span> encode_pid() &lt;span style="color:#f92672">|&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">String&lt;/span>&lt;span style="color:#f92672">.&lt;/span>replace_prefix(&lt;span style="color:#e6db74">&amp;#34;PID&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> %{&lt;span style="color:#e6db74">field&lt;/span>: &lt;span style="color:#e6db74">:clients_count&lt;/span>, &lt;span style="color:#e6db74">header&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Clients count&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">sortable&lt;/span>: &lt;span style="color:#e6db74">:asc&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defp&lt;/span> row_attrs(session) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#e6db74">&amp;#34;phx-click&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;show_info&amp;#34;&lt;/span>},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#e6db74">&amp;#34;phx-value-info&amp;#34;&lt;/span>, encode_pid(session[&lt;span style="color:#e6db74">:pid&lt;/span>])},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#e6db74">&amp;#34;phx-page-loading&amp;#34;&lt;/span>, &lt;span style="color:#66d9ef">true&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
The main ingredients of this implementation are:&lt;/p>
&lt;ul>
&lt;li>The &lt;code>use Phoenix.LiveDashboard.PageBuilder&lt;/code> directive, which adopts the behaviour with the same name and imports some convenience functions useful for building pages (e.g. &lt;code>encode_pid/1&lt;/code>).&lt;/li>
&lt;li>The &lt;code>menu_link/2&lt;/code> callback, which is used to define the name of the page and its label in the top navigation bar.&lt;/li>
&lt;li>The &lt;code>render_page/2&lt;/code> callback, which has to return a valid &lt;a href="https://hexdocs.pm/phoenix_live_dashboard/Phoenix.LiveDashboard.PageBuilder.html#t:component/0">&lt;code>component&lt;/code>&lt;/a> - in this case via the &lt;a href="https://hexdocs.pm/phoenix_live_dashboard/Phoenix.LiveDashboard.PageBuilder.html#table/1">&lt;code>table/1&lt;/code>&lt;/a> function.&lt;/li>
&lt;/ul>
&lt;p>The table definition has a few moving parts:&lt;/p>
&lt;ul>
&lt;li>An &lt;code>id&lt;/code> (unique among other Live Dashboard pages).&lt;/li>
&lt;li>A &lt;code>title&lt;/code>, shown in the page.&lt;/li>
&lt;li>A &lt;code>rows_name&lt;/code>, interpolated in the short text blurb that details the total amount of results.&lt;/li>
&lt;li>A &lt;code>columns&lt;/code> attribute, which is a list of maps detailing the properties of each column.
For each column, the &lt;code>id&lt;/code> property has to map to a key in the data we will use to populate the table.
The &lt;code>sortable&lt;/code> property defines which column can be used for sorting (by clicking on the header chevron). Note that unless you specify a &lt;code>default_sort_by&lt;/code> attribute for the entire table, you have to have at least one column with the &lt;code>sortable&lt;/code> property defined, otherwise you will get a compile error.
The &lt;code>format&lt;/code> function takes the raw value for a cell in the column and transforms it to a string. It&amp;#39;s useful to provide a string representation of the value that is suitable for an HTML table. In the code above, we copy the format function defined in &lt;a href="https://github.com/phoenixframework/phoenix_live_dashboard/blob/8d7148d9c333a27766ee8bc971d4dba93c0f9695/lib/phoenix/live_dashboard/pages/processes_page.ex#L34">the Processes Live Dashboard page&lt;/a>.&lt;/li>
&lt;li>A &lt;code>row_attrs&lt;/code> function, which takes the data for each row and has to return a list of tuples representing the Phoenix LiveView attributes to apply to the table row itself. Defining attribute is necessary to enable functionality activated by clicking on the row itself. The implementation in this example lets you inspect the session PID in a modal overlay.
Similar to the &lt;code>format&lt;/code> function, we leverage &lt;code>encode_pid/1&lt;/code> to format the PID as string compatible with the &lt;code>show_info&lt;/code> LiveView event.&lt;/li>
&lt;li>A &lt;code>row_fetcher&lt;/code> function, which takes the current &lt;code>params&lt;/code> (search query, limit, sort key, sort direction) and the current node, and returns the data used to populate the table.
The return value has to conform to a tuple shape where the first value is a list of sessions (in the shape of maps with the same keys used for column ids) and the second value is the total number of results (irrespectively of the limit).
As we implemented &lt;code>Tune.Spotify.Supervisor.sessions/0&lt;/code> taking care of using the same key names, its return value perfectly fits the expectations of the &lt;code>row_fetcher&lt;/code> function.&lt;/li>
&lt;/ul>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-5" class="outline-2">
&lt;h2 id="headline-5">
Mounting the dashboard page
&lt;/h2>
&lt;div id="outline-text-headline-5" class="outline-text-2">
&lt;p>
To have the page up and running, we need to modify the &lt;code>live_dashboard/2&lt;/code> function inside the application router:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>live_dashboard &lt;span style="color:#e6db74">&amp;#34;/dashboard&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">metrics&lt;/span>: &lt;span style="color:#a6e22e">TuneWeb.Telemetry&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">metrics_history&lt;/span>: {&lt;span style="color:#a6e22e">TuneWeb.Telemetry.Storage&lt;/span>, &lt;span style="color:#e6db74">:metrics_history&lt;/span>, []},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">additional_pages&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">spotify_sessions&lt;/span>: &lt;span style="color:#a6e22e">TuneWeb.LiveDashboard.SpotifySessionsPage&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-6" class="outline-2">
&lt;h2 id="headline-6">
Filters and limits
&lt;/h2>
&lt;div id="outline-text-headline-6" class="outline-text-2">
&lt;p>
We can now focus on implementing search, sorting and limits. Conceptually, we need to:&lt;/p>
&lt;ul>
&lt;li>If specified, apply the search filter.&lt;/li>
&lt;li>Always apply sort order.&lt;/li>
&lt;li>Count the sorted elements, to return the correct total.&lt;/li>
&lt;li>Always apply the limit clause to the sorted elements.&lt;/li>
&lt;/ul>
&lt;p>All of these operations have to be handled by the implementation of the &lt;code>row_fetcher&lt;/code> function.&lt;/p>
&lt;p>
The params map has the following keys:&lt;/p>
&lt;ul>
&lt;li>&lt;code>:search&lt;/code>: the string representing the contents of the search input (or &lt;code>nil&lt;/code> when empty).&lt;/li>
&lt;li>&lt;code>:sort_by&lt;/code>: the id of the column to sort by.&lt;/li>
&lt;li>&lt;code>:sort_dir&lt;/code>: the sort direction, expressed with the atoms &lt;code>:asc&lt;/code> and &lt;code>:desc&lt;/code>.&lt;/li>
&lt;li>&lt;code>:limit&lt;/code>: the integer value representing the amount of max items requested by the user.&lt;/li>
&lt;/ul>
&lt;p>The params map is very well thought out, as it has a fixed structure, applied defaults where available and values that play well with functions from the &lt;code>Enum&lt;/code> module.&lt;/p>
&lt;p>
We can extend the &lt;code>fetch_sessions/2&lt;/code> function as follows:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">defmodule&lt;/span> &lt;span style="color:#a6e22e">TuneWeb.LiveDashboard.SpotifySessionsPage&lt;/span> &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># omitted&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defp&lt;/span> fetch_sessions(params, _node) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sessions &lt;span style="color:#f92672">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Tune.Spotify.Supervisor&lt;/span>&lt;span style="color:#f92672">.&lt;/span>sessions()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">|&amp;gt;&lt;/span> filter(params)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#a6e22e">Enum&lt;/span>&lt;span style="color:#f92672">.&lt;/span>take(sessions, params[&lt;span style="color:#e6db74">:limit&lt;/span>]), length(sessions)}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defp&lt;/span> filter(sessions, params) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sessions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">|&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Enum&lt;/span>&lt;span style="color:#f92672">.&lt;/span>filter(&lt;span style="color:#66d9ef">fn&lt;/span> session &lt;span style="color:#f92672">-&amp;gt;&lt;/span> session_match?(session, params[&lt;span style="color:#e6db74">:search&lt;/span>]) &lt;span style="color:#66d9ef">end&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">|&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Enum&lt;/span>&lt;span style="color:#f92672">.&lt;/span>sort_by(&lt;span style="color:#66d9ef">fn&lt;/span> session &lt;span style="color:#f92672">-&amp;gt;&lt;/span> session[params[&lt;span style="color:#e6db74">:sort_by&lt;/span>]] &lt;span style="color:#66d9ef">end&lt;/span>, params[&lt;span style="color:#e6db74">:sort_dir&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defp&lt;/span> session_match?(_session, &lt;span style="color:#66d9ef">nil&lt;/span>), &lt;span style="color:#e6db74">do&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defp&lt;/span> session_match?(session, search_string), &lt;span style="color:#e6db74">do&lt;/span>: &lt;span style="color:#a6e22e">String&lt;/span>&lt;span style="color:#f92672">.&lt;/span>contains?(session[&lt;span style="color:#e6db74">:id&lt;/span>], search_string)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
As outlined above, we start by filtering by search, using a very simple logic that just checks if the session ID contains the searched string.&lt;/p>
&lt;p>
After search, we apply the sorting logic: the values of the &lt;code>:sort_by&lt;/code> and &lt;code>:sort_dir&lt;/code> perfectly fit using &lt;code>Enum.sort_by/3&lt;/code> (a really appreciated API design choice), making the implementation short and sweet.&lt;/p>
&lt;p>
When defining the returning tuple, we take care of applying the limit and returning the correct total count.&lt;/p>
&lt;p>
With these changes in place, the generated table behaves as expected:&lt;/p>
&lt;p>
&lt;img src="https://claudio-ortolina.org/img/building-a-custom-page-for-phoenix-live-dashboard/sessions-table.png" alt="A screenshot of the Spotify sessions table built in this blog post" class="left" />
&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-7" class="outline-2">
&lt;h2 id="headline-7">
Supporting multiple nodes
&lt;/h2>
&lt;div id="outline-text-headline-7" class="outline-text-2">
&lt;p>
The last piece of the puzzle is making sure that we take into account the currently selected node.&lt;/p>
&lt;p>
Fortunately, we just need to make a very small change to &lt;code>fetch_sessions/2&lt;/code>:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">defp&lt;/span> fetch_sessions(params, node) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sessions &lt;span style="color:#f92672">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> node
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">|&amp;gt;&lt;/span> &lt;span style="color:#e6db74">:rpc&lt;/span>&lt;span style="color:#f92672">.&lt;/span>call(&lt;span style="color:#a6e22e">Tune.Spotify.Supervisor&lt;/span>, &lt;span style="color:#e6db74">:sessions&lt;/span>, [])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">|&amp;gt;&lt;/span> filter(params)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#a6e22e">Enum&lt;/span>&lt;span style="color:#f92672">.&lt;/span>take(sessions, params[&lt;span style="color:#e6db74">:limit&lt;/span>]), length(sessions)}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
The OTP &lt;a href="https://erlang.org/doc/man/rpc.html">rpc&lt;/a> module conveniently provides a &lt;a href="https://erlang.org/doc/man/rpc.html#call-4">&lt;code>call/4&lt;/code>&lt;/a> function that takes a node name, module, function, and arguments, returning the exact same value of the remotely executed function.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-8" class="outline-2">
&lt;h2 id="headline-8">
Conclusions
&lt;/h2>
&lt;div id="outline-text-headline-8" class="outline-text-2">
&lt;p>
To see the final version of &lt;code>TuneWeb.LiveDashboard.SpotifySessionsPage&lt;/code>, you can open &lt;a href="https://github.com/fully-forged/tune/blob/32038997bc89f94ca8ee18f80d2f1cae946f7acb/lib/tune_web/live_dashboard/spotify_sessions_page.ex">the file in the repo&lt;/a>.&lt;/p>
&lt;/div>
&lt;/div></description></item><item><title>Tips for Finch, Telemetry, and Phoenix Live Dashboard</title><link>https://claudio-ortolina.org/posts/tips-for-finch-and-telemetry/</link><pubDate>Tue, 17 Nov 2020 17:27:05 +0000</pubDate><guid>https://claudio-ortolina.org/posts/tips-for-finch-and-telemetry/</guid><description>
&lt;p>
While working on &lt;a href="https://github.com/fully-forged/tune">Tune&lt;/a>, I needed to collect performance metrics related to the interaction with the Spotify API.&lt;/p>
&lt;p>
The Finch HTTP client &lt;a href="https://hexdocs.pm/finch/Finch.html#module-telemetry">exposes Telemetry metrics&lt;/a>, which made it very easy to display them via &lt;a href="https://hex.pm/packages/phoenix_live_dashboard">Phoenix Live Dashboard&lt;/a>.&lt;/p>
&lt;p>
Starting from the stock &lt;code>TuneWeb.Telemetry&lt;/code> file generated by Phoenix (see &lt;a href="https://hexdocs.pm/phoenix/telemetry.html#content">the official guides for an explanation&lt;/a>), I just added two new summary metrics to the &lt;code>metrics/0&lt;/code> function:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span> summary(&lt;span style="color:#e6db74">&amp;#34;vm.total_run_queue_lengths.io&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># HTTP&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> summary(&lt;span style="color:#e6db74">&amp;#34;finch.request.stop.duration&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">unit&lt;/span>: {&lt;span style="color:#e6db74">:native&lt;/span>, &lt;span style="color:#e6db74">:millisecond&lt;/span>}, &lt;span style="color:#e6db74">tags&lt;/span>: [&lt;span style="color:#e6db74">:path&lt;/span>]),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> summary(&lt;span style="color:#e6db74">&amp;#34;finch.response.stop.duration&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">unit&lt;/span>: {&lt;span style="color:#e6db74">:native&lt;/span>, &lt;span style="color:#e6db74">:millisecond&lt;/span>}, &lt;span style="color:#e6db74">tags&lt;/span>: [&lt;span style="color:#e6db74">:path&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
With this change in place (&lt;a href="https://github.com/fully-forged/tune/commit/7c573aa30313a8adf1954076b9cd957f0f910155">commit&lt;/a>), I had all metrics being visualized in the dashboard, grouped by the Spotify API path. I wanted to make some improvements:&lt;/p>
&lt;ul>
&lt;li>The &lt;code>:path&lt;/code> tag includes query string parameters, so calls like &lt;code>search?q=marillion&lt;/code> and &lt;code>search?q=fish&lt;/code> would be aggregated in different groups. Instead, I would want them to be part of the same group, ignoring query string parameters.&lt;/li>
&lt;li>Since I &lt;a href="https://claudio-ortolina.org/posts/using-finch-with-sentry/">setup Sentry to use Finch as a client&lt;/a>, I wanted to exclude calls made to Sentry and only have charts reporting metrics about the interaction with Spotify&lt;/li>
&lt;/ul>
&lt;div id="outline-container-headline-1" class="outline-2">
&lt;h2 id="headline-1">
Aggregating by normalized path
&lt;/h2>
&lt;div id="outline-text-headline-1" class="outline-text-2">
&lt;p>
To aggregate metrics by normalized path, we can apply a transformation function to the metric tag values, generate a normalized path tag and use that to aggregate metrics. As shown &lt;a href="https://hexdocs.pm/telemetry_metrics/Telemetry.Metrics.html#module-metrics">in the telemetry_metrics docs&lt;/a>, the option we need is &lt;code>tag_values&lt;/code>:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> metrics &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># omitted&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> summary(&lt;span style="color:#e6db74">&amp;#34;finch.request.stop.duration&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">unit&lt;/span>: {&lt;span style="color:#e6db74">:native&lt;/span>, &lt;span style="color:#e6db74">:millisecond&lt;/span>},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">tags&lt;/span>: [&lt;span style="color:#e6db74">:normalized_path&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">tag_values&lt;/span>: &lt;span style="color:#f92672">&amp;amp;&lt;/span>add_normalized_path&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">defp&lt;/span> add_normalized_path(metadata) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Map&lt;/span>&lt;span style="color:#f92672">.&lt;/span>put(metadata, &lt;span style="color:#e6db74">:normalized_path&lt;/span>, &lt;span style="color:#a6e22e">URI&lt;/span>&lt;span style="color:#f92672">.&lt;/span>parse(metadata&lt;span style="color:#f92672">.&lt;/span>path)&lt;span style="color:#f92672">.&lt;/span>path)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
We can use the built-in &lt;code>URI&lt;/code> module to parse normalized path out of the Finch metric metadata and add it to the metadata itself. With that in place, we can update the &lt;code>tags&lt;/code> option to reference &lt;code>:normalized_path&lt;/code>. With this change, metrics are aggregated on the endpoint only, without any query string. For reference, here&amp;#39;s the relevant &lt;a href="https://github.com/fully-forged/tune/commit/8ab6fab59357e97579ac086a94e768193c2872a5?branch=8ab6fab59357e97579ac086a94e768193c2872a5&amp;amp;diff=unified">commit&lt;/a>.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-2" class="outline-2">
&lt;h2 id="headline-2">
Filtering only Spotify calls
&lt;/h2>
&lt;div id="outline-text-headline-2" class="outline-text-2">
&lt;p>
To filter for Spotify calls only, we can use the &lt;code>keep&lt;/code> option, which specifies a predicate function that can be used to define which metrics should be kept and which ones should be discarded. Discarded metrics will not appear in the dashboard chart.&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">def&lt;/span> metrics &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># omitted&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> summary(&lt;span style="color:#e6db74">&amp;#34;finch.response.stop.duration&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">unit&lt;/span>: {&lt;span style="color:#e6db74">:native&lt;/span>, &lt;span style="color:#e6db74">:millisecond&lt;/span>},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">tags&lt;/span>: [&lt;span style="color:#e6db74">:normalized_path&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">tag_values&lt;/span>: &lt;span style="color:#f92672">&amp;amp;&lt;/span>add_normalized_path&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">keep&lt;/span>: &lt;span style="color:#f92672">&amp;amp;&lt;/span>keep_spotify&lt;span style="color:#f92672">/&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">reporter_options&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">nav&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;HTTP - Spotify&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">defp&lt;/span> keep_spotify(meta) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> meta&lt;span style="color:#f92672">.&lt;/span>host &lt;span style="color:#f92672">=~&lt;/span> &lt;span style="color:#e6db74">&amp;#34;spotify&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
As the meta information already includes a host, we can compare it with the &lt;code>spotify&lt;/code> string. The &lt;code>=~&lt;/code> operator makes the comparison a bit more resilient, so that we don&amp;#39;t have to worry about the exact hostname, rather a hostname related to Spotify. This choice might need to be revised if we ever end up interacting via HTTP with another service with &amp;#34;spotify&amp;#34; in their host name (unlikely, but possible).&lt;/p>
&lt;p>
For some additional clarity, we can also use the &lt;code>nav&lt;/code> reporter option (see &lt;a href="https://hexdocs.pm/phoenix_live_dashboard/metrics.html#reporter-options">Phoenix LiveDashboard documentation&lt;/a> for more details) to make sure that the navigation header displays a name that details the additional filter applied to the HTTP metrics. For reference, see the relevant &lt;a href="https://github.com/fully-forged/tune/commit/c9f483d93c0813c0e680a4aaf2a88fed0851334f#diff-f599bf85f0cafc16b50f0e1a561b6aa39e4ab256fb6d43e8726619570866c5b1">commit&lt;/a>.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-3" class="outline-2">
&lt;h2 id="headline-3">
Conclusion
&lt;/h2>
&lt;div id="outline-text-headline-3" class="outline-text-2">
&lt;p>
Both improvements required very small updates. Here&amp;#39;s the final result, showing the custom Nav title (&amp;#34;HTTP - Spotify&amp;#34;) to hint at the filter used to only show Spotify calls, and aggregation by normalized path (without query string).&lt;/p>
&lt;p>
&lt;img src="https://claudio-ortolina.org/img/tips-for-finch-and-telemetry/charts.png" alt="A screenshot of the configured Finch Metrics inside Live Dashboard" class="left" />
&lt;/p>
&lt;p>
All in all, I was pleased to see that it was straightforward to customise the charts I needed. One thing I haven&amp;#39;t worked on yet is aggregating metrics by logical path, i.e. by route (&lt;code>GET /artist/:id&lt;/code>) instead of individual paths (&lt;code>GET /artist/123&lt;/code>), but I have some ideas and will come back on it in a future post.&lt;/p>
&lt;/div>
&lt;/div></description></item><item><title>Using Finch With Sentry</title><link>https://claudio-ortolina.org/posts/using-finch-with-sentry/</link><pubDate>Tue, 10 Nov 2020 08:41:30 +0000</pubDate><guid>https://claudio-ortolina.org/posts/using-finch-with-sentry/</guid><description>
&lt;p>
A few weeks ago I added enabled support for &lt;a href="https://sentry.io">Sentry&lt;/a> inside &lt;a href="https://github.com/fully-forged/tune">Tune&lt;/a>, my Spotify browser/client. Even if I&amp;#39;m pretty much the only user (I built it for myself after all), having exception tracking has already proved to be useful - band and song names can really create all sorts of issues.&lt;/p>
&lt;p>
The &lt;a href="https://hex.pm/packages/sentry">official Sentry package&lt;/a> works as advertised and by default it communicates using &lt;a href="https://hex.pm/packages/hackney">Hackney&lt;/a> as a http client. As I&amp;#39;ve been using &lt;a href="https://hex.pm/packages/finch">Finch&lt;/a> in the project, I was pleased to see that Sentry exposed a &lt;code>client&lt;/code> configuration option that allowed using your own module, as long as it implemented the &lt;code>Sentry.HTTPClient&lt;/code> behaviour.&lt;/p>
&lt;p>
The advantage in swapping the http client library (on top of uniforming the building blocks of the application) is that Finch has support for &lt;a href="https://hex.pm/packages/telemetry">Telemetry&lt;/a> metrics.&lt;/p>
&lt;blockquote>
&lt;p>Update #1: Thanks to Wojtek Mach for &lt;a href="https://github.com/fully-forged/tune/pull/131">a more streamlined implementation.&lt;/a>&lt;/p>
&lt;/blockquote>
&lt;p>
The module I wrote is quite short:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">defmodule&lt;/span> &lt;span style="color:#a6e22e">Sentry.FinchClient&lt;/span> &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@moduledoc&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> Defines a small shim to use `Finch` as a `Sentry.HTTPClient`.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#e6db74"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@behaviour&lt;/span> &lt;span style="color:#a6e22e">Sentry.HTTPClient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@impl&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> child_spec &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Finch&lt;/span>&lt;span style="color:#f92672">.&lt;/span>child_spec(&lt;span style="color:#e6db74">name&lt;/span>: &lt;span style="color:#a6e22e">Sentry.Finch&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">@impl&lt;/span> &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> post(url, headers, body) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> request &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">Finch&lt;/span>&lt;span style="color:#f92672">.&lt;/span>build(&lt;span style="color:#e6db74">:post&lt;/span>, url, headers, body)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">Finch&lt;/span>&lt;span style="color:#f92672">.&lt;/span>request(request, &lt;span style="color:#a6e22e">Sentry.Finch&lt;/span>) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#e6db74">:ok&lt;/span>, response} &lt;span style="color:#f92672">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#e6db74">:ok&lt;/span>, response&lt;span style="color:#f92672">.&lt;/span>status, response&lt;span style="color:#f92672">.&lt;/span>headers, response&lt;span style="color:#f92672">.&lt;/span>body}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> error &lt;span style="color:#f92672">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
The trickiest bit was to get the &lt;code>child_spec/0&lt;/code> callback right while keeping &lt;a href="https://erlang.org/doc/man/dialyzer.html">dialyzer&lt;/a> happy. The first implementation I wrote was simply &lt;code>{Finch, name: Sentry.Finch}&lt;/code>, but that would fail to satisfy &lt;a href="https://hexdocs.pm/sentry/Sentry.HTTPClient.html#c:child_spec/0">the typespec defined for &lt;code>child_spec/0&lt;/code>&lt;/a>. I then switched to a more verbose version:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> child_spec &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> opts &lt;span style="color:#f92672">=&lt;/span> [&lt;span style="color:#e6db74">name&lt;/span>: &lt;span style="color:#a6e22e">Sentry.Finch&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Supervisor&lt;/span>&lt;span style="color:#f92672">.&lt;/span>child_spec(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> %{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">id&lt;/span>: __MODULE__,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">start&lt;/span>: {&lt;span style="color:#a6e22e">Finch&lt;/span>, &lt;span style="color:#e6db74">:start_link&lt;/span>, [opts]},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">type&lt;/span>: &lt;span style="color:#e6db74">:supervisor&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
This version satisfied dialyzer, but turns out there&amp;#39;s a simpler way. After publishing this blog post, Wojtek Mach reached out and submitted a PR to streamline the specification to the version shown in the full example above.&lt;/p>
&lt;p>
I also updated my production configuration:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span>config &lt;span style="color:#e6db74">:sentry&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">dsn&lt;/span>: {&lt;span style="color:#e6db74">:system&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;SENTRY_DSN&amp;#34;&lt;/span>},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">environment_name&lt;/span>: &lt;span style="color:#e6db74">:prod&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">enable_source_code_context&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">root_source_code_path&lt;/span>: &lt;span style="color:#a6e22e">File&lt;/span>&lt;span style="color:#f92672">.&lt;/span>cwd!(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">client&lt;/span>: &lt;span style="color:#a6e22e">Sentry.FinchClient&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">included_environments&lt;/span>: [&lt;span style="color:#e6db74">:prod&lt;/span>]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
You can of course expand on this implementation if you need to pass more options to the &lt;code>Finch&lt;/code> child specification - I found that for my use case, defaults are fine, so for now I don&amp;#39;t need to add any configuration hooks.&lt;/p>
&lt;p>
To see the change in context, &lt;a href="https://github.com/fully-forged/tune/pull/122">this is the original PR&lt;/a>, with the &lt;a href="https://github.com/fully-forged/tune/pull/131">follow-up by Wojtek Mach&lt;/a>.&lt;/p></description></item><item><title>Fish</title><link>https://claudio-ortolina.org/posts/fish/</link><pubDate>Fri, 06 Nov 2020 12:28:16 +0000</pubDate><guid>https://claudio-ortolina.org/posts/fish/</guid><description>
&lt;img src="https://claudio-ortolina.org/img/fish/cover.jpg"/>
&lt;div id="outline-container-headline-1" class="outline-3">
&lt;h3 id="headline-1">
Prelude
&lt;/h3>
&lt;div id="outline-text-headline-1" class="outline-text-3">
&lt;p>
I have very early memories of rock music in my life. Since I was a toddler, my parents (and particularly my dad) got me used to listen to 80s rock music. Bands like Queen, Guns N&amp;#39; Roses, AC/DC, Van Halen, and Bon Jovi are ingrained in my memories as sounds of my childhood.&lt;/p>
&lt;p>
It&amp;#39;s not surprising that my taste in music has developed from there, branching out over the years in metal and progressive rock. Music is a constant companion of my daily life and contributed to the formation of my identity.&lt;/p>
&lt;p>
My taste changed and adapted: around ten years ago I was dismissing &lt;a href="https://en.wikipedia.org/wiki/Operation:_Mindcrime">&lt;em>Operation: Mindcrime&lt;/em>&lt;/a> by &lt;a href="https://en.wikipedia.org/wiki/Queensrÿche">Queensrÿche&lt;/a> as a mediocre album, but two years ago I picked it up again and it was a revelation. Something similar happened while listening to &lt;a href="https://en.wikipedia.org/wiki/Rage_Against_the_Machine">Rage Against the Machine&lt;/a>, &lt;a href="https://en.wikipedia.org/wiki/Tool">Tool&lt;/a> or &lt;a href="https://en.wikipedia.org/wiki/A_Perfect_Circle">A Perfect Circle&lt;/a>, didn&amp;#39;t like them years ago, absolutely love them now.&lt;/p>
&lt;p>
This change got me thinking - why do these bands resonate with me now?&lt;/p>
&lt;p>
I think the answer lies primarily in the integrity of the message connected to these bands: their perspective, political stance, perspective on life really struck a cord with me in my 30s. They take an explicit position around issues I care about.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-2" class="outline-3">
&lt;h3 id="headline-2">
Enter Marillion and Fish
&lt;/h3>
&lt;div id="outline-text-headline-2" class="outline-text-3">
&lt;p>
&lt;img src="https://claudio-ortolina.org/img/fish/fish-photo.jpg" alt="Fish&amp;#39;s portrait" class="left" style="float: left; margin-right: 1rem;" />
&lt;/p>
&lt;p>
Until a year ago, I had a very cursory knowledge of &lt;a href="https://en.wikipedia.org/wiki/Marillion">Marillion&lt;/a>&amp;#39;s body of work. I kney they existed, that they have a substantial discography and that they represent an important page in the history of 80s and 90s prog.&lt;/p>
&lt;p>
One morning, Spotify&amp;#39;s suggestions algorithm decided to play &lt;a href="https://www.youtube.com/watch?v=lalBmbrWEvQ">&lt;em>Incommunicado&lt;/em>&lt;/a> and &lt;a href="https://www.youtube.com/watch?v=6COmtBk6lYo">&lt;em>Sugar Mice&lt;/em>&lt;/a> in the space of an hour. I found myself stopping what I was doing in both instances - there was something about those songs that really dragged my attention. I loved the music - but the lyrics stopped me in my tracks.&lt;/p>
&lt;p>
&lt;em>Incommunicado&lt;/em> is about ambition, the drive to make it big without taking responsibilities. In the context of the entire album, it&amp;#39;s both a metaphor for the difficult life of Torch, the main character whose life is falling to pieces, but also a direct description of the music business.&lt;/p>
&lt;p>
&lt;em>Sugar Mice&lt;/em> is a song about the devastating effects of unemployment and economic recession. The title refers to a popular english sweet made of sugar. The main refrain &amp;#34;We&amp;#39;re just sugar mice in the rain&amp;#34; gives a very vivid representation of how fragile life can be.&lt;/p>
&lt;p>
The words, references, and metaphors included in the lyrics stood out as something completely out of the ordinary - it &lt;span style="text-decoration: underline;">sounded&lt;/span> like poetry.&lt;/p>
&lt;p>
The more I explored other Marillion&amp;#39;s songs, the more I realized that what really resonated with me was &lt;a href="https://en.wikipedia.org/wiki/Fish_(singer)">Fish&lt;/a>, their initial singer and lyricist. Following his body of work after he left Marillion, I kept finding incredible songs.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-3" class="outline-3">
&lt;h3 id="headline-3">
Love and relationships
&lt;/h3>
&lt;div id="outline-text-headline-3" class="outline-text-3">
&lt;blockquote>
&lt;p>Do you remember?&lt;br>
Barefoot on the lawn with shooting stars&lt;br>
Do you remember?&lt;br>
The loving on the floor in Belsize Park&lt;br>
Do you remember?&lt;br>
Dancing in stilettoes in the snow&lt;br>
Do you remember?&lt;br>
You never understood I had to go&lt;br>
By the way, didn&amp;#39;t I break your heart?&lt;br>
Please excuse me, I never meant to break your heart&lt;br>
So sorry, I never meant to break your heart&lt;br>
But you broke mine — &lt;em>Kayleigh, 1985&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>
By far the most succesful Marillion song, &lt;em>Kayleigh&lt;/em> is often cited as Fish&amp;#39;s apology to different women for the failure of their romantic relationships, a collage of vivid images and melancholic moments.&lt;/p>
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/OQ4oaLUilBc?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
To me, this song sows the seeds of a theme that goes through a few songs written by Fish: work/life balance. Relationships fail when you divert your attention away from them, when you don&amp;#39;t put in everyday work to keep them going.&lt;/p>
&lt;p>
In &lt;em>Zoë 25&lt;/em> it&amp;#39;s the aftermath of another relationship that didn&amp;#39;t go well:&lt;/p>
&lt;blockquote>
&lt;p>When you&amp;#39;re looking for somebody, you might not even see them, &lt;br>
When they&amp;#39;re standing there in front of you, right before your eyes, &lt;br>
If you&amp;#39;re looking for somebody you&amp;#39;re gonna need some help, &lt;br>
You know you&amp;#39;ll never find her when you&amp;#39;re looking for yourself. — &lt;em>Zoë 25, 2007&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>
In &lt;em>Garden of Remembrance&lt;/em>, it&amp;#39;s Alzheimer&amp;#39;s disease, something completely outside our control:&lt;/p>
&lt;blockquote>
&lt;p>He&amp;#39;s lost between the here and now&lt;br>
Somewhere that he can&amp;#39;t be found&lt;br>
She&amp;#39;s still here&lt;br>
Her love a ghost of memory&lt;br>
She&amp;#39;ll wait for an eternity&lt;br>
He&amp;#39;s still here — &lt;em>Garden of Remembrance, 2020&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/-RwwU8Nvs1g?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"
>&lt;/iframe>
&lt;/div>
&lt;/p>
&lt;p>
In &lt;em>cliche&lt;/em>, the song lyrics use estabilished cliches to acknowledge that no matter how much hard you try, sometimes the simplest thing you can say is what matters.&lt;/p>
&lt;blockquote>
&lt;p>That&amp;#39;s why I&amp;#39;m trying to say with my deepest sincerity&lt;br>
That&amp;#39;s why I&amp;#39;m finding it comes down to the basic simplicities&lt;br>
The best way is with an old cliche&lt;br>
It&amp;#39;s simply the best way is with an old cliche&lt;br>
Always the best way is with an old cliche&lt;br>
I&amp;#39;ll leave it to the best way, it&amp;#39;s an old cliche&lt;br>
I love you. — &lt;em>Cliche, 1990&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>
In &lt;em>Punch and Judy&lt;/em>&lt;sup class="footnote-reference">&lt;a id="footnote-reference-1" href="#footnote-1">1&lt;/a>&lt;/sup> it&amp;#39;s the progressive deterioration of a relationship, escalating in murdering fantasies:&lt;/p>
&lt;blockquote>
&lt;p>Whatever happened to morning smiles,&lt;br>
Whatever happened to wicked wiles, permissive styles,&lt;br>
Whatever happened to twinkling eyes,&lt;br>
Whatever happened to hard fast drives,&lt;br>
Complements on unnatural size&lt;/p>
&lt;p>
Punch, Punch, Punch And Judy, Punch, Punch, Punch And Judy&lt;br>
Punch, Punch, Punch And Judy.&lt;/p>
&lt;p>
Propping up a bar, family car,&lt;br>
Sweating out a mortgage as a balding clerk,&lt;br>
Punch And Judy, [Judy]&lt;br>
World war three, suburbanshee,&lt;br>
Just slip her these pills and I&amp;#39;ll be free.&lt;/p>
&lt;p>
No more Judy, Judy. Judy no more! Goodbye Judy! — &lt;em>Punch and Judy, 1984&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-4" class="outline-3">
&lt;h3 id="headline-4">
Living on your own terms
&lt;/h3>
&lt;div id="outline-text-headline-4" class="outline-text-3">
&lt;p>
Another recurring topic is the idea of living on your own terms. From &lt;em>Tongues&lt;/em>, where Fish lets out the frustration of dealing with lawyers during a very long lawsuit with music publisher EMI:&lt;/p>
&lt;blockquote>
&lt;p>Your entrenched opinions,&lt;br>
On the border of arrogance,&lt;br>
Dug in against the compromise.&lt;br>
A position indefensible, your actions illogical&lt;br>
You&amp;#39;re speaking in tongues&lt;/p>
&lt;p>
You swear contradictions&lt;br>
Your tedious monologues, wielding authority,&lt;br>
Demanding subservience, demanding&lt;br>
I make your sense.&lt;br>
Demanding speaking in tongues. — &lt;em>Tongues, 1991&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>
The inability to speak the same language becomes a massive blocker. The aforementioned lawsuit with EMI ended up being a fundamental event in Fish&amp;#39;s life, pushing him to pursue the ownership of all rights of his solo albums (except the first one, &lt;em>Vigil in a Wilderness of Mirrors&lt;/em>).&lt;/p>
&lt;p>
In &lt;em>Circle Line&lt;/em>&lt;sup class="footnote-reference">&lt;a id="footnote-reference-2" href="#footnote-2">2&lt;/a>&lt;/sup>, it&amp;#39;s the awareness of the 9-to-5 grind that is imposed on the majority of us:&lt;/p>
&lt;blockquote>
&lt;p>9 to 5&amp;#39;s the only time I try to kid myself that I&amp;#39;m still alive,&lt;br>
That I&amp;#39;m living out the dream to earn my freedom from this rat race&lt;br>
Where all I do&amp;#39;s survive, I live the lie, I serve my time.&lt;/p>
&lt;p>
The circle line.&lt;/p>
&lt;p>
Just another day, just another day, just another day,&lt;br>
Just another day, just another day, just another day on the circle line.&lt;/p>
&lt;p>
The circle line, on the circle line. — &lt;em>Circle Line, 2007&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>
&lt;em>Lost Plot&lt;/em>, on losing track of what matters:&lt;/p>
&lt;blockquote>
&lt;p>I was blinded by light but the vision had died, I&amp;#39;d forgotten&lt;br>
In time just what I was fighting for&lt;br>
I&amp;#39;d forgotten who&amp;#39;s side I was on, the difference between&lt;br>
Right and wrong&lt;br>
I was out of my depth, going out of my mind, going down in&lt;br>
A field where no prisoners are taken, no quarter is given&lt;br>
The writing was small, it burned on the wall, I&amp;#39;d sold out&lt;br>
My soul for what it was worth&lt;br>
I&amp;#39;d lost the plot, my number was up, the game was over&lt;br>
Snakes and ladders, a world of snakes and ladders, snakes&lt;br>
And ladders — &lt;em>Lost Plot, 2004&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>
&lt;em>View from the Hill&lt;/em>, where we the hill is an endless collection of things that don&amp;#39;t matter and just keep us imprisoned.&lt;/p>
&lt;p>
&lt;figure class="center" >
&lt;img src="https://claudio-ortolina.org/img/fish/the-hill.jpg" alt="Illustration of the Hill, one of the main metaphors in the album Vigil in a Wilderness of Mirrors" />
&lt;figcaption class="center" >Artwork for the album of Vigil in the Wilderness of Mirrors, by Mark Wilkinson&lt;/figcaption>
&lt;/figure>
&lt;/p>
&lt;blockquote>
&lt;p>You sit and think that everything is coming up roses&lt;br>
But you can&amp;#39;t see the weeds that entangle your feet&lt;br>
You can&amp;#39;t see the wood for the trees &amp;#39;cause the forest is burning&lt;br>
And you say it&amp;#39;s the smoke in your eyes that&amp;#39;s making you cry&lt;/p>
&lt;p>
They sold you the view from a hill&lt;br>
They told you that the view from the hill would be&lt;br>
Further than you have ever seen before&lt;br>
They sold you a view from a hill&lt;br>
They sold you a view from a hill — &lt;em>View from the Hill, 1990&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-5" class="outline-3">
&lt;h3 id="headline-5">
The system has failed (us)
&lt;/h3>
&lt;div id="outline-text-headline-5" class="outline-text-3">
&lt;p>
Fish has never shied away from commenting on politics: from &lt;em>Market Square Heroes&lt;/em> (Marillion&amp;#39;s first single) to &lt;em>Weltschmerz&lt;/em>, the ending track of his latest (and last) album.&lt;/p>
&lt;p>
&lt;em>Market Square Heroes&lt;/em>&lt;sup class="footnote-reference">&lt;a id="footnote-reference-3" href="#footnote-3">3&lt;/a>&lt;/sup> is once again an anthem of an angry generation that suffers the consequences of austerity and recession:&lt;/p>
&lt;blockquote>
&lt;p>I give peace signs when I wage war in the disco&lt;br>
I&amp;#39;m the warrior in the ultra violet haze&lt;br>
Armed with antisocial insecurity&lt;br>
I plan the path of destiny from this maze&lt;/p>
&lt;p>
Cause I&amp;#39;m a Market Square hero gathering the storms to troop&lt;br>
Cause I&amp;#39;m a Market Square hero speeding the beat of the street pulse&lt;br>
Are you following me, are you following me?&lt;br>
Well suffer my fallen angels and follow me&lt;br>
I&amp;#39;m the Market Square hero, I&amp;#39;m the Market Square hero&lt;br>
We are Market Square Heroes, to be Market Square Heroes — &lt;em>Market Square Heroes, 1982&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>
&lt;em>Weltschmerz&lt;/em> is a summary of all fights worth fighting - from climate change, to poverty, to the general failure of a political system that emphasized polarisation and division:&lt;/p>
&lt;blockquote>
&lt;p>I am a grey bearded warrior, a poet of no mean acclaim&lt;br>
My words are my weapons that I proffer with disdain&lt;br>
My melancholy aspect is something you can’t disregard&lt;br>
My motives you cannot question nor my strong sense of right and wrong\\&lt;/p>
&lt;p>
I’ve formed the opinion that things can’t stay as they are&lt;br>
My anger and my fury trapped like a wasp in a jar&lt;br>
It’s never too late to make a brave new start&lt;br>
When the revolution is called I will play my part — &lt;em>Weltschmerz, 2020&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-6" class="outline-3">
&lt;h3 id="headline-6">
Perfume River
&lt;/h3>
&lt;div id="outline-text-headline-6" class="outline-text-3">
&lt;p>
&lt;em>Perfume River&lt;/em>&lt;sup class="footnote-reference">&lt;a id="footnote-reference-4" href="#footnote-4">4&lt;/a>&lt;/sup> deserves a mention on its own: in this song, Fish looks at the consequences of the Vietnam War, whose images are burned in his childhood memory.&lt;/p>
&lt;p>
&lt;figure class="center" >
&lt;img src="https://claudio-ortolina.org/img/fish/feast-of-consequences.jpg" alt="Artwork for the album Feast of Consequences" />
&lt;figcaption class="center" >Artwork for the album Feast of Consequences by Mark Wilkinson&lt;/figcaption>
&lt;/figure>
&lt;/p>
&lt;blockquote>
&lt;p>Fire breathing dragons swarm in sweltering skies, spewing flame on innocents below&lt;br>
Charred and brittle corpses, blackened evidence, I am enraged, I am afraid, I am forlorn&lt;br>
The ashes from wise pages fly from libraries, tumble in the clouds of smoke and flies&lt;br>
To lie as dust in corners of dark palaces, the fetid smell of revolution haunts the air.&lt;/p>
&lt;p>
Take me away to the Perfume River; carry me down to the perfume river&lt;br>
Set me adrift on a well-stocked open boat&lt;br>
Show me the way to the Perfume River, send me away down the perfume river&lt;br>
Pour that sweet, sweet liquor down my throat; pour it down my throat — &lt;em>Perfume River, 2013&lt;/em>&lt;/p>
&lt;/blockquote>
&lt;p>
Once again, the images evoked are incredibly strong, full of colour - red flames and rage, smoke, dust and death.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-headline-7" class="outline-3">
&lt;h3 id="headline-7">
Visuals
&lt;/h3>
&lt;div id="outline-text-headline-7" class="outline-text-3">
&lt;p>
A powerful ingredient in Fish&amp;#39;s artistic work has always been his collaboration with &lt;a href="https://www.the-masque.com/mainpage.html">Mark Wilkinson&lt;/a>, who illustrated all Marillion&amp;#39;s albums (until Fish left the band) and all of Fish&amp;#39;s work. His surrealistic style is unmistakable and perfectly complements the &amp;#34;visual&amp;#34; nature of a lot of Fish&amp;#39;s songs.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div class="footnotes">
&lt;hr class="footnotes-separatator">
&lt;div class="footnote-definitions">
&lt;div class="footnote-definition">
&lt;sup id="footnote-1">&lt;a href="#footnote-reference-1">1&lt;/a>&lt;/sup>
&lt;div class="footnote-body">
&lt;p>The title refers to Punch and Judy, the main characters of a puppet show popular in British culture. See &lt;a href="https://en.wikipedia.org/wiki/Punch_and_Judy">the Wikipedia page for more information.&lt;/a>&lt;/p>
&lt;/div>
&lt;/div>
&lt;div class="footnote-definition">
&lt;sup id="footnote-2">&lt;a href="#footnote-reference-2">2&lt;/a>&lt;/sup>
&lt;div class="footnote-body">
&lt;p>The Circle Line is one of London&amp;#39;s Underground lines.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div class="footnote-definition">
&lt;sup id="footnote-3">&lt;a href="#footnote-reference-3">3&lt;/a>&lt;/sup>
&lt;div class="footnote-body">
&lt;p>The title is both a reference to a market square in Ailesbury, an english town, and Nietzsche&amp;#39;s &lt;a href="https://sourcebooks.fordham.edu/mod/nietzsche-madman.asp">Parable of the Madman&lt;/a>.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div class="footnote-definition">
&lt;sup id="footnote-4">&lt;a href="#footnote-reference-4">4&lt;/a>&lt;/sup>
&lt;div class="footnote-body">
&lt;p>The title refers to a river running through the city of Huế in Vietnam, one of the cities deeply affected by the Vietnam War.&lt;/p>
&lt;/div>
&lt;/div>
&lt;/div>
&lt;/div></description></item><item><title>Remote 1-on-1 meetings</title><link>https://claudio-ortolina.org/posts/one-on-one-notes/</link><pubDate>Wed, 04 Nov 2020 09:10:38 +0000</pubDate><guid>https://claudio-ortolina.org/posts/one-on-one-notes/</guid><description>
&lt;p>
In the last two years I&amp;#39;ve done hundreds of remote 1-on-1 meetings, both
as a contributor talking to my manager(s) and as a manager talking to
people in my team.&lt;/p>
&lt;p>
As a manager, I consider 1-on-1 meetings the most important
responsibility I have: empowering other people to do their best work can
greatly outmeasure any contribution I can give on my own.&lt;/p>
&lt;p>
What follows is some notes on what seems to work for me. It does not
represent by any means a proper research on the matter, so please take
it with a grain of salt.&lt;/p>
&lt;div id="outline-container-long-term-objectives" class="outline-3">
&lt;h3 id="long-term-objectives">
Long term objectives
&lt;/h3>
&lt;div id="outline-text-long-term-objectives" class="outline-text-3">
&lt;p>
Regular 1-on-1 meetings can certainly be used to solve immediate,
short-term blockers or clarify some questions, but the long term
objective is building a working professional relationship where:&lt;/p>
&lt;ul>
&lt;li>Both parties can safely be vulnerable and address deep issues that
make day to day work more difficult. This can be anything from anxiety
about the world at large, to having to homeschool kids, to
difficulties in multitasking different responsibilities. Note that as
a manager, you&amp;#39;re allowed to show that you&amp;#39;re struggling to the other
person. We&amp;#39;re human after all and that helps building transparent
relationships. Needless to say, confidentiality and respect are
paramount.&lt;/li>
&lt;li>Trust takes time and effort. It may take months before people open up
and start addressing important topics: what motivates them, problems
they see in the team and in the company, their own aspirations and
ultimately work on their professional growth.&lt;/li>
&lt;li>Lead by example: if you ask a complex question (e.g. what do you wanna
focus on this year?), answer it yourself first, so that they
understand better.&lt;/li>
&lt;li>Always prepare something to talk about: even if it seems artificial,
it will gradually tune your perception to be on the lookout between
meetings for topics to talk about and it will get easier. At the same
time, if the other party doesn&amp;#39;t provide topics over multiple
meetings, make sure you address that. There&amp;#39;s always something to talk
about.&lt;/li>
&lt;/ul>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-preparation" class="outline-3">
&lt;h3 id="preparation">
Preparation
&lt;/h3>
&lt;div id="outline-text-preparation" class="outline-text-3">
&lt;p>
Regular 1-on-1 meetings require preparation: the easiest thing to do to
ease preparation is to keep an agenda document shared between the two
participants.&lt;/p>
&lt;p>
I can recommend a shared Google Doc, with sections titled by date and
sorted in reverse chronological order (most recent first).&lt;/p>
&lt;p>
This way you can:&lt;/p>
&lt;ol>
&lt;li>Predictably add topics to discuss between meetings (it&amp;#39;s always at
the top).&lt;/li>
&lt;li>Have implicit time tracking, i.e. you know when something was
discussed.&lt;/li>
&lt;li>Easily collaborate in real time during the meeting to capture ideas
in a form that correctly reflects the other person&amp;#39;s thoughts.&lt;/li>
&lt;/ol>
&lt;p>This system is by no means perfect (especially if you end up having
dozens of these documents), but it&amp;#39;s flexible enough that you can adapt
it to individual preferences. For example, I find that with some people
we can use the agenda to discuss topic asynchronously in writing
beforehand (and draw some conclusions in the meeting), while with other
we defer discussion to the meeting itself. With other ones, we end up
adding topics during the meeting as the conversation naturally goes in
different directions.&lt;/p>
&lt;p>
In addition, it&amp;#39;s important to prepare for the conversation, even for
just 5-10 minutes before the meeting. Having such time buffer not only
helps clarifying thoughts beforehand, but also gives you an opportunity
to stop thinking about what you were doing before and what you&amp;#39;re gonna
be doing after the meeting.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-during-the-meeting" class="outline-3">
&lt;h3 id="during-the-meeting">
During the meeting
&lt;/h3>
&lt;div id="outline-text-during-the-meeting" class="outline-text-3">
&lt;ol>
&lt;li>Respect the agenda as much as you can, making sure that you either
address all points on it or explicitly say &amp;#34;We&amp;#39;ll need to address
this in a separate call. Can it wait our next planned session or do
you wanna schedule an earlier follow-up?&amp;#34;.&lt;/li>
&lt;li>If the conversation drifts to a completely different direction and
time is running tight, make sure both parties are happy to discuss
that direction at the expense of other topics. You always have the
option to capture the new topic and address it at a later stage.&lt;/li>
&lt;li>Be at ease in saying &amp;#34;I don&amp;#39;t know, but here&amp;#39;s how I plan to find
out and here&amp;#39;s when I&amp;#39;m gonna report back about it&amp;#34;. At the end of
the day, trust is built on transparency and predictability.&lt;/li>
&lt;li>When the other person explains something to you and you wanna make
sure you got it right, repeat it back and ask for confirmation. By
doing that, you both check your understanding and implicitly ask the
other person to check their own explanation. I&amp;#39;ve used this
technique in all sorts of other conversations in my professional and
personal life and it does absolute wonders in clarifying
expectations on both sides.&lt;/li>
&lt;li>If you take notes, learn how to do that quickly by using keywords,
then fill in the blanks later (and ask for confirmation to the other
person once done). Writing can break the flow of the conversation,
so it needs to be done carefully in order to minimize its impact.&lt;/li>
&lt;li>If a conversation topic implies someone taking an action, explicitly
state that in the form of &amp;#34;I will&amp;#34; or &amp;#34;You will&amp;#34; or &amp;#34;Someone else
will&amp;#34;, with an indication of when that would happen.&lt;/li>
&lt;li>If the meeting resulted in some actions, recap them at the end of
the call.&lt;/li>
&lt;li>Video conversations have a different pace - let people speak, listen
carefully, slow down, repeat a few times if needed.&lt;/li>
&lt;li>Explicitly ask the other person if they think a topic has been
exhausted before moving on.&lt;/li>
&lt;li>Respectfully ask the other person how they&amp;#39;re doing, leaving them
space to decide what they can comfortably share with you. Do no pry
and always qualify your questions with the reason why you&amp;#39;re asking
them. For example, I&amp;#39;ve been recently asking people how they&amp;#39;re
dealing with the COVID-19 pandemic, because I noticed erratic
working patterns that suggest they may be working too much (for many
people, work is much simpler to deal with, so they end up using it
as a safe haven - I&amp;#39;m not a psychologist though so this is another
thing to take with a grain of salt).&lt;/li>
&lt;li>Provide context: while this is important in any company, I believe it&amp;#39;s fundamental
in a remote company because people have more limited opportunities to gather
context by casually taking part to unscheduled conversations. So if I&amp;#39;m
discussing a specific project that I think it&amp;#39;s connected to other projects,
I&amp;#39;ll share that. More often than not, the person on the other end will
appreciate the additional information and will make good use of it.&lt;/li>
&lt;/ol>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-after-the-meeting" class="outline-3">
&lt;h3 id="after-the-meeting">
After the meeting
&lt;/h3>
&lt;div id="outline-text-after-the-meeting" class="outline-text-3">
&lt;p>
If you have any action, just do it as early as possible. Your ability to
follow up is by far the most important factor in building trust. If the
other person asks you to do something, you agree to it and you don&amp;#39;t,
they will not ask you again.&lt;/p>
&lt;p>
If at any point you realize you didn&amp;#39;t do something you promised to do,
acknowledge your shortcoming, apologize and make up for it. It happens,
and if you&amp;#39;re transparent about it usually the other person will
understand.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-topics" class="outline-3">
&lt;h3 id="topics">
Topics
&lt;/h3>
&lt;div id="outline-text-topics" class="outline-text-3">
&lt;p>
1-on-1 meetings are structured around the people involved: while you can
definitely start from some guideline questions, they should over time
develop into a unique conversation.&lt;/p>
&lt;p>
That said, over the course of multiple meetings you should aim at:&lt;/p>
&lt;ul>
&lt;li>Unblocking specific issue related to current streams of work,
e.g. &amp;#34;I&amp;#39;m undecided on how to build X, if using this or that.&amp;#34;&lt;/li>
&lt;li>Clarify responsibilities, e.g. &amp;#34;Yes, you need to take care of X, while
Alice can take care of Y.&amp;#34;&lt;/li>
&lt;li>Provide feedback on work done, e.g. &amp;#34;I really liked how you did X
because…&amp;#34; or &amp;#34;I&amp;#39;d like to speak about Y, as there&amp;#39;s an opportunity
to improve Z.&amp;#34;&lt;/li>
&lt;li>Useful things to learn about, e.g. &amp;#34;As you&amp;#39;re working on X, you might
enjoy learning about Y.&amp;#34;&lt;/li>
&lt;li>Connect the dots with other projects, e.g. &amp;#34;As you&amp;#39;re working on X,
you might be interested to speak to Alice, as she&amp;#39;s working on Y,
which is related to X as…&amp;#34;&lt;/li>
&lt;li>Happiness, satisfaction, and future work e.g. &amp;#34;If we look at the
roadmap, there&amp;#39;s X, Y, Z. Do they interest you? Which one would be
your initial preference to work on?&amp;#34;&lt;/li>
&lt;/ul>
&lt;p>One important aspect is balance: too often 1-on-1 meetings are focused on the
day to day work and don&amp;#39;t cover the larger picture. This is why there should be
scheduled Feedback Sessions where you go through some meta-questions that allow
expanding scope.&lt;/p>
&lt;p>
These are some examples of questions useful for those sessions:&lt;/p>
&lt;ol>
&lt;li>Are you happy about the work you&amp;#39;re doing? Is it satisfactory?&lt;/li>
&lt;li>Looking at X time period, can you point out a piece of your work you&amp;#39;re proud of?&lt;/li>
&lt;li>Looking at the same time period, can you point out 3 team
achievements you&amp;#39;re proud of?&lt;/li>
&lt;li>What should the team focus on in this quarter?&lt;/li>
&lt;li>If you had a magic wand and could instantly change anything in the
team, what would that be?&lt;/li>
&lt;/ol>
&lt;p>In general, Feedback Sessions are an opportunity to look at the larger picture
and think about the future. For more inspiration, you can consult the &lt;a href="https://help.small-improvements.com/article/264-24-questions-to-ask-in-your-next-11-meeting">Small
Improvements guide to 1-on-1 meetings&lt;/a>.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-conclusions" class="outline-3">
&lt;h3 id="conclusions">
Conclusions
&lt;/h3>
&lt;div id="outline-text-conclusions" class="outline-text-3">
&lt;p>
As mentioned before, this is by no means an exhaustive guide, but a collection
on thoughts based on my experience. At the end of the day, if you always focus
on listening to the other person and acting swiftly on their feedback, you will
get good results.&lt;/p>
&lt;/div>
&lt;/div></description></item><item><title>A Short Profiling Story</title><link>https://claudio-ortolina.org/posts/a-short-profiling-story/</link><pubDate>Tue, 03 Nov 2020 11:42:42 +0000</pubDate><guid>https://claudio-ortolina.org/posts/a-short-profiling-story/</guid><description>
&lt;p>
While transcribing
&lt;a href="https://www.elixirconf.eu/talks/The-Perils-of-Large-Files/">the talk I
gave at the last ElixirConf.eu&lt;/a> conference, one of my colleagues
pointed out that I glossed over the details of one of the examples. This
prompted me to do some digging and I want to share what I found.&lt;/p>
&lt;div id="outline-container-the-problem" class="outline-3">
&lt;h3 id="the-problem">
The problem
&lt;/h3>
&lt;div id="outline-text-the-problem" class="outline-text-3">
&lt;p>
The example in question is a module responsible to fetch a file from a
remote source and write it at the specified path.&lt;/p>
&lt;p>
The implementation is very simplistic and lacks both error handling and
retry logic.&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defmodule&lt;/span> &lt;span style="color:#a6e22e">Perils.Examples.Store&lt;/span> &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> write(file_name, url) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> with {&lt;span style="color:#e6db74">:ok&lt;/span>, data} &lt;span style="color:#f92672">&amp;lt;-&lt;/span> get(url) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">File&lt;/span>&lt;span style="color:#f92672">.&lt;/span>write!(file_name, data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defp&lt;/span> get(url) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">:httpc&lt;/span>&lt;span style="color:#f92672">.&lt;/span>request(&lt;span style="color:#e6db74">:get&lt;/span>, {&lt;span style="color:#a6e22e">String&lt;/span>&lt;span style="color:#f92672">.&lt;/span>to_charlist(url), []}, [], [])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">|&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#e6db74">:ok&lt;/span>, result} &lt;span style="color:#f92672">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{_, &lt;span style="color:#ae81ff">200&lt;/span>, _}, _headers, body} &lt;span style="color:#f92672">=&lt;/span> result
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#e6db74">:ok&lt;/span>, body}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> error &lt;span style="color:#f92672">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
Looking at the code, we can see that it relies on &lt;code class="verbatim">:httpc&lt;/code>, the http
client that ships with Erlang/OTP.&lt;/p>
&lt;p>
Both in my talk and in the initial transcription draft, I pointed out
that running this code with a 12MB file would result in a memory usage
peak at around 350/375MB, but didn&amp;#39;t really look into why.&lt;/p>
&lt;p>
&lt;img src="https://claudio-ortolina.org/img/a-short-profiling-story/before.png" alt="A chart visualizing a 350MB memory spike" class="left" />
&lt;/p>
&lt;p>
Such delta between the file size and peak memory usage is suspicious and
worth investigating.&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-the-investigation" class="outline-3">
&lt;h3 id="the-investigation">
The investigation
&lt;/h3>
&lt;div id="outline-text-the-investigation" class="outline-text-3">
&lt;p>
I started by setting up an &lt;a href="https://github.com/parroty/exprof">exprof&lt;/a>
test, so that I could profile resource usage associated with the
problematic function.&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">defmodule&lt;/span> &lt;span style="color:#a6e22e">A&lt;/span> &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#a6e22e">ExProf.Macro&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> run(url) &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> profile &lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Perils.Examples.Store&lt;/span>&lt;span style="color:#f92672">.&lt;/span>write(&lt;span style="color:#e6db74">&amp;#34;magazine.pdf&amp;#34;&lt;/span>, url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;https://web-examples.pspdfkit.com/magazine/example.pdf&amp;#34;&lt;/span> &lt;span style="color:#75715e">#12MB&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {records, _block_result} &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">A&lt;/span>&lt;span style="color:#f92672">.&lt;/span>run(url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> total_percent &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">Enum&lt;/span>&lt;span style="color:#f92672">.&lt;/span>reduce(records, &lt;span style="color:#ae81ff">0.0&lt;/span>, &lt;span style="color:#f92672">&amp;amp;&lt;/span>(&amp;amp;1&lt;span style="color:#f92672">.&lt;/span>percent &lt;span style="color:#f92672">+&lt;/span> &amp;amp;2))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">IO&lt;/span>&lt;span style="color:#f92672">.&lt;/span>inspect(&lt;span style="color:#e6db74">&amp;#34;total = &lt;/span>&lt;span style="color:#e6db74">#{&lt;/span>total_percent&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
The result (with some lines omitted) shows that most of the time
(51.74%) is spent converting the binary response body to a list inside
the &lt;code class="verbatim">maybe_format_body/2&lt;/code> function:&lt;/p>
&lt;div class="src src-text">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span> FUNCTION CALLS % TIME [uS / CALLS]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> -------- ----- ------- ---- [----------]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;omitted&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> erlang:iolist_to_binary/1 1 20.46 49705 [ 49705.00]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> erlang:binary_to_list/1 1 27.54 66887 [ 66887.00]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> httpc:maybe_format_body/2 1 51.74 125664 [ 125664.00]&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
While this is not an indication of higher memory usage per se, it&amp;#39;s a
good lead: binary to list conversion can be memory intensive.&lt;/p>
&lt;p>
I then looked at the
&lt;a href="https://github.com/erlang/otp/blob/3f21ce1e6a5d6c548867fa4bc9a8c666c626ade1/lib/inets/src/http_client/httpc.erl#L655-L661">source
for &lt;code class="verbatim">maybe_format_body/2&lt;/code>&lt;/a>, making sure to match on the OTP version I
tested against (23.1.1).&lt;/p>
&lt;div class="src src-erlang">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-erlang" data-lang="erlang">&lt;span style="display:flex;">&lt;span> maybe_format_body(BinBody, Options) &lt;span style="color:#f92672">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> proplists:&lt;span style="color:#a6e22e">get_value&lt;/span>(body_format, Options, string) &lt;span style="color:#66d9ef">of&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> string &lt;span style="color:#f92672">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> binary_to_list(BinBody);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ &lt;span style="color:#f92672">-&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BinBody
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">end&lt;/span>.&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
As expected, the function uses &lt;code class="verbatim">binary_to_list/1&lt;/code> to transform the
response binary body into a list. Luckily, this behaviour can be tweaked
via the &lt;code class="verbatim">body_format&lt;/code> option, which defaults to &lt;code class="verbatim">string&lt;/code> (as in Erlang
string, which maps to a character list in Elixir).&lt;/p>
&lt;p>
Searching for &lt;code class="verbatim">body_format&lt;/code> in
&lt;a href="http://erlang.org/doc/man/httpc.html#request-5">the Erlang docs for
&lt;code class="verbatim">request/5&lt;/code>&lt;/a> shows that indeed it&amp;#39;s possible to tweak our problematic
implementation to:&lt;/p>
&lt;div class="src src-elixir">
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-elixir" data-lang="elixir">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">:httpc&lt;/span>&lt;span style="color:#f92672">.&lt;/span>request(&lt;span style="color:#e6db74">:get&lt;/span>, {&lt;span style="color:#a6e22e">String&lt;/span>&lt;span style="color:#f92672">.&lt;/span>to_charlist(url), []}, [], &lt;span style="color:#e6db74">body_format&lt;/span>: &lt;span style="color:#e6db74">:binary&lt;/span>)&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;/div>
&lt;p>
With this change, memory usage decreases dramatically, showing a delta
only slightly larger than the file size.&lt;/p>
&lt;p>
&lt;img src="https://claudio-ortolina.org/img/a-short-profiling-story/after.png" alt="A chart visualizing a 15MB memory spike" class="left" />
&lt;/p>
&lt;/div>
&lt;/div>
&lt;div id="outline-container-conclusion" class="outline-3">
&lt;h3 id="conclusion">
Conclusion
&lt;/h3>
&lt;div id="outline-text-conclusion" class="outline-text-3">
&lt;p>
This whole investigation got me thinking, as the &lt;code class="verbatim">body_format&lt;/code> option
had been in the docs all along, yet I hadn&amp;#39;t seen it. I can find three
reasons:&lt;/p>
&lt;ol>
&lt;li>The overall logic in the example doesn&amp;#39;t really care about the
response body contents, as it just writes them to a file. Without
seeing that response, there was no way for me to even notice its
type.&lt;/li>
&lt;li>&lt;code class="verbatim">File.write/2&lt;/code> accepts binaries, strings and character lists - again
I didn&amp;#39;t have a reason to even wonder about the type used to
represent that returned response body.&lt;/li>
&lt;li>Working primarily in Elixir, everything tends to be either a string
or a binary. I just &amp;#34;forget&amp;#34; that character lists exist, which lead
to the implicit assumption that this would be the default for
&lt;code class="verbatim">:httpc&lt;/code>.&lt;/li>
&lt;/ol>
&lt;p>In other words, I didn&amp;#39;t know what to search in the docs. Profiling
tools helped me understand the problem space and pointed me in the right
direction.&lt;/p>
&lt;/div>
&lt;/div></description></item></channel></rss>