<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ptagl</title>
    <description>The latest articles on DEV Community by Ptagl (@ptagl).</description>
    <link>https://dev.to/ptagl</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3706332%2Fa52cb61b-2669-4157-9921-511f985f785d.png</url>
      <title>DEV Community: Ptagl</title>
      <link>https://dev.to/ptagl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ptagl"/>
    <language>en</language>
    <item>
      <title>Profiling Rust applications with `tracing`</title>
      <dc:creator>Ptagl</dc:creator>
      <pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ptagl/profiling-rust-applications-with-tracing-19jf</link>
      <guid>https://dev.to/ptagl/profiling-rust-applications-with-tracing-19jf</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When I think about &lt;strong&gt;profiling&lt;/strong&gt;, I do not think about optimization first. I think about visibility and &lt;strong&gt;understanding&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Profiling is obviously useful when &lt;strong&gt;performance&lt;/strong&gt; matters, but that is only half of the story. In practice, it is also one of the best ways to understand how a system actually &lt;strong&gt;behaves&lt;/strong&gt;: a function may be called more often than expected, an initialization path may run twice, or a step you assumed was negligible may end up dominating the whole execution.&lt;/p&gt;

&lt;p&gt;In this post I will use &lt;code&gt;profiling&lt;/code&gt; and &lt;code&gt;tracing&lt;/code&gt; in Rust to generate a &lt;strong&gt;JSON trace&lt;/strong&gt; that I can open in &lt;strong&gt;Perfetto UI&lt;/strong&gt;. Perfetto can visualize external traces in Chrome JSON format, which makes it a very convenient way to inspect the output produced by &lt;code&gt;tracing_chrome&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick introduction to the &lt;code&gt;tracing&lt;/code&gt; and &lt;code&gt;profiling&lt;/code&gt; crates
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tracing&lt;/code&gt; is widely used in Rust for &lt;strong&gt;logging&lt;/strong&gt; and diagnostics, but its scope is broader than traditional logging. It provides structured &lt;strong&gt;events&lt;/strong&gt; and &lt;strong&gt;spans&lt;/strong&gt;, which makes it useful not only for recording what happened, but also for tracking execution flow, preserving context, and collecting timing information for &lt;strong&gt;profiling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;profiling&lt;/code&gt;, on the other hand, is a thin &lt;strong&gt;abstraction layer&lt;/strong&gt; that lets you instrument code in a generic way, without tying your application directly to a specific &lt;strong&gt;profiling backend&lt;/strong&gt;. In this post I use it together with &lt;code&gt;tracing&lt;/code&gt;, but it can work with other profilers as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating custom spans
&lt;/h2&gt;

&lt;p&gt;The most direct way to instrument a specific part of your code is to create a &lt;strong&gt;custom span&lt;/strong&gt; around it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Some code&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;profiling&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;scope!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Main Application Scope"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Code to be tracked&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Some other code&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is simple, but also surprisingly effective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrumenting functions
&lt;/h2&gt;

&lt;p&gt;Of course, real world applications are more complex than this dummy &lt;code&gt;main&lt;/code&gt; function, and it would be inconvenient to call this macro in tens or hundreds of places around the codebase. Luckily, there is a macro that can be used to instrument whole &lt;strong&gt;functions&lt;/strong&gt; very quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[profiling::function]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;very_important_function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Marking functions with this attribute will make the profiler keep track of every call to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrumenting implementations
&lt;/h2&gt;

&lt;p&gt;Even this approach could become annoying if it needs to be replicated for several functions to be instrumented.&lt;/p&gt;

&lt;p&gt;There is another attribute that comes in handy to simplify our job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;VeryImportantStruct&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="nd"&gt;#[profiling::all_functions]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;VeryImportantStruct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;function1&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;function2&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;function3&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells &lt;code&gt;profiling&lt;/code&gt; to instrument &lt;strong&gt;every function&lt;/strong&gt; found in the implementation body at once, making it way more concise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initializing the profiling and exporting the trace
&lt;/h2&gt;

&lt;p&gt;Most of the job is already done, but we need one more step before being able to collect data from our application.&lt;/p&gt;

&lt;p&gt;We need to &lt;strong&gt;initialize&lt;/strong&gt; the profiling, specifying the format we need (JSON file compatible with Chrome tracing in this case) and decide when to start and stop collecting data.&lt;/p&gt;

&lt;p&gt;It is typically done early in the &lt;code&gt;main&lt;/code&gt; function or in some other initialization function, but it can also be started later or even on-demand if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tracing_chrome&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ChromeLayerBuilder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tracing_subscriber&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nn"&gt;layer&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SubscriberExt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;util&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SubscriberInitExt&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chrome_layer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ChromeLayerBuilder&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nn"&gt;tracing_subscriber&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chrome_layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;guard&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// The rest of the application logic&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tracing_chrome&lt;/code&gt; provides a layer for tracing-subscriber that writes traces in Chrome trace format. Profiling starts immediately after the &lt;code&gt;init()&lt;/code&gt; call, and the trace file is finalized when the guard returned by the builder is dropped, so it is important to keep it alive for the whole duration of the profiling session.&lt;/p&gt;

&lt;p&gt;If it's &lt;strong&gt;dropped earlier&lt;/strong&gt; for any reason (e.g. the guard is placed in a dedicated initialization function without being returned), the profiling &lt;strong&gt;stops&lt;/strong&gt;. The result of such an error is typically the creation of &lt;strong&gt;empty&lt;/strong&gt; JSON traces!&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyzing the trace
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;full example&lt;/strong&gt; of instrumented Rust application can be found on my GitHub profile. Once everything is set up, run the application and wait for it to stop. At that point, you should see a new &lt;strong&gt;JSON file&lt;/strong&gt; generated in the current working directory with a name like &lt;code&gt;trace-1773521455925975.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There are several ways to open this trace, for instance with the &lt;strong&gt;viewer&lt;/strong&gt; built into Chrome (&lt;code&gt;chrome://tracing&lt;/code&gt;) or with Perfetto UI.&lt;/p&gt;

&lt;p&gt;Personally, I prefer &lt;a href="https://ui.perfetto.dev/" rel="noopener noreferrer"&gt;Perfetto&lt;/a&gt; because it provides a modern, clean, and fast UI.&lt;/p&gt;

&lt;p&gt;Once you open the trace in Perfetto UI, you get a &lt;strong&gt;timeline&lt;/strong&gt; view of the recorded spans, which gives you an immediate overview of what happened over the lifetime of the application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuujtiskn9pvcysfmfp80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuujtiskn9pvcysfmfp80.png" alt="Perfetto trace timeline"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By &lt;strong&gt;selecting&lt;/strong&gt; spans or time ranges, you can &lt;strong&gt;inspect&lt;/strong&gt; durations, nesting, timestamps, and the relationships between different parts of the execution.&lt;/p&gt;

&lt;p&gt;That is the kind of detail that is easy to miss in logs and &lt;strong&gt;easy&lt;/strong&gt; to spot in a &lt;strong&gt;trace&lt;/strong&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to develop a simple Chrome extension</title>
      <dc:creator>Ptagl</dc:creator>
      <pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ptagl/how-to-develop-a-simple-chrome-extension-28f3</link>
      <guid>https://dev.to/ptagl/how-to-develop-a-simple-chrome-extension-28f3</guid>
      <description>&lt;p&gt;Among all the online services I use, &lt;a href="https://intervals.icu" rel="noopener noreferrer"&gt;Intervals.icu&lt;/a&gt; is one of my favorites when it comes to &lt;strong&gt;cycling&lt;/strong&gt;.&lt;br&gt;
It's a free service where I track my training, analyze power and HR zones, and plan the workouts for the week.&lt;br&gt;
Since &lt;strong&gt;AI assistants&lt;/strong&gt; became actually useful, I started feeding them my workout data to get a second opinion on training load, recovery, and race readiness.&lt;/p&gt;

&lt;p&gt;Unfortunately, there was no &lt;strong&gt;one-click&lt;/strong&gt; way to &lt;strong&gt;export&lt;/strong&gt; an activity. My workflow was quite slow and inefficient:&lt;br&gt;
open the activity, copy the summary at the top, switch tab, copy the power zone breakdown, switch again, grab the HR zones, then paste everything into a chat window and hope the AI would make sense of the mess.&lt;/p&gt;

&lt;p&gt;What I really wanted was a &lt;strong&gt;single button&lt;/strong&gt; that would collect all that data as structured JSON and put it in my clipboard.&lt;/p&gt;

&lt;p&gt;That button didn't exist, and given that the project is not open source, all I could do was to &lt;strong&gt;build&lt;/strong&gt; it, or, to better say, "inject" it into the web page through a Chrome extension.&lt;/p&gt;
&lt;h2&gt;
  
  
  The skeleton
&lt;/h2&gt;

&lt;p&gt;The web is full of &lt;strong&gt;starter templates&lt;/strong&gt; for Chrome extensions.&lt;br&gt;
You don't need to write the boilerplate yourself: grab any "hello world" extension from GitHub or the &lt;a href="https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world" rel="noopener noreferrer"&gt;official docs&lt;/a&gt;, and you'll have a working folder structure in minutes.&lt;/p&gt;

&lt;p&gt;The only file that actually matters for this use case is &lt;code&gt;content.js&lt;/code&gt;, the &lt;strong&gt;script&lt;/strong&gt; that runs inside the target page.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three steps to inject anything into websites
&lt;/h2&gt;

&lt;p&gt;The whole logic fits in three steps, and they apply to basically any site you want to extend:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Make sure you're on the right page.&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;manifest.json&lt;/code&gt; already restricts your script to run only on matching URLs (via the &lt;code&gt;matches&lt;/code&gt; field in &lt;code&gt;content_scripts&lt;/code&gt;).&lt;br&gt;
But sometimes you need a finer check inside the script itself, for example, Intervals.icu activity URLs follow a pattern like &lt;code&gt;/activity/XXXXXXXX&lt;/code&gt;, so a quick &lt;code&gt;window.location.pathname&lt;/code&gt; check does the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Find a DOM anchor.&lt;/strong&gt;&lt;br&gt;
You need somewhere to &lt;strong&gt;attach&lt;/strong&gt; your new element. Open DevTools, inspect the page, and find a stable &lt;strong&gt;container&lt;/strong&gt; near where you want the button to appear. Try to find an element that is easy to &lt;strong&gt;select&lt;/strong&gt; and won't change often.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Add your logic.&lt;/strong&gt;&lt;br&gt;
In my case: call the Intervals.icu &lt;strong&gt;API&lt;/strong&gt; to fetch the full activity JSON, then copy it to the &lt;strong&gt;clipboard&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here are the most important parts of the implementation:&lt;/p&gt;

&lt;p&gt;1) Identify the current activity and fetch its JSON&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getActivityData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Activity id is the last segment of the current path&lt;/span&gt;
  &lt;span class="c1"&gt;// e.g. /activity/12345 -&amp;gt; "12345"&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activityId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Intervals.icu endpoint for a single activity&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://intervals.icu/api/activity/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;activityId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiUrl&lt;/span&gt; &lt;span class="cm"&gt;/* (credentials/cookies are handled by the browser session) */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`API request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Find a DOM anchor where we can place our button&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getButtonContainer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Pick a stable container in the activity header area.&lt;/span&gt;
  &lt;span class="c1"&gt;// This is the "anchor" where we attach our UI.&lt;/span&gt;
  &lt;span class="c1"&gt;//&lt;/span&gt;
  &lt;span class="c1"&gt;// NOTE: selectors are site-specific and may break if the UI changes.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.gutter8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Walk down the DOM to the right section (site-specific).&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rightSection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastElementChild&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastSection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rightSection&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;lastElementChild&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lastSection&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3) Inject the button and wire up the click logic&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addExportButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Avoid duplicating the button if we re-run this function.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;EXPORT_BUTTON_ID&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getButtonContainer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Minimal wrapper to fit the existing layout (site-specific styling).&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buttonGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;buttonGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;float: right; padding-right: 12px;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Reuse the site's button classes to blend in.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exportButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;exportButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;EXPORT_BUTTON_ID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;exportButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;btn btn-default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;exportButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;exportButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JSON to clipboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;exportButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Fetch JSON and copy to clipboard&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activityJSON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getActivityData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activityJSON&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Tiny UX feedback: swap label for 2 seconds&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;exportButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;exportButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Copied!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exportButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;originalText&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;buttonGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;exportButton&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buttonGroup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4) Wait for the DOM to be ready and inject the button&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Use a mutation observer to detect when the page is ready for the injection&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getButtonContainer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nf"&gt;addExportButton&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtree&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Run once as well (in case the DOM is already ready).&lt;/span&gt;
&lt;span class="nf"&gt;addExportButton&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full source is available on &lt;a href="https://github.com/ptagl/intervals.icu-json-to-clipboard" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing without publishing
&lt;/h2&gt;

&lt;p&gt;You don't need a developer account to try it out. Just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Collect all the extension &lt;strong&gt;files&lt;/strong&gt; (in particular &lt;code&gt;manifest.json&lt;/code&gt; and &lt;code&gt;content.js&lt;/code&gt;) into a folder accessible to the browser&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open chrome://extensions or the &lt;strong&gt;extension page&lt;/strong&gt; of your chromium-based browser&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enable &lt;strong&gt;Developer mode&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;code&gt;Load unpacked&lt;/code&gt; and point it at your folder. It's done, the extension is &lt;strong&gt;live&lt;/strong&gt; in your browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing on the Chrome Web Store
&lt;/h2&gt;

&lt;p&gt;When you're ready to &lt;strong&gt;share&lt;/strong&gt; it, the process is straightforward but has a few steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create a developer account at the Chrome Developer Dashboard&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pay the one-time $5 &lt;strong&gt;registration fee&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Upload&lt;/strong&gt; your ZIP, fill in the store listing (description, screenshots, icons, and other information)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Submit for review
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;review&lt;/strong&gt; can take anywhere from a few hours to a few days. Google's official &lt;a href="https://developer.chrome.com/docs/extensions/develop/migrate/publish-mv3" rel="noopener noreferrer"&gt;publish guide&lt;/a&gt; walks you through every field.&lt;br&gt;
It's tedious but not complicated.&lt;/p&gt;

&lt;p&gt;The whole thing took me a couple of evenings. The extension is tiny, a single &lt;code&gt;content.js&lt;/code&gt; and a manifest file, but it &lt;strong&gt;sped up&lt;/strong&gt; my workflow hugely.&lt;br&gt;
If you use Intervals.icu and an AI assistant, give it a try.&lt;br&gt;
And if you want to &lt;strong&gt;inject&lt;/strong&gt; something into a different site, the pattern is exactly the same.&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>extension</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to use the cache with GitHub Actions - The right way</title>
      <dc:creator>Ptagl</dc:creator>
      <pubDate>Tue, 27 Jan 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ptagl/how-to-use-the-cache-with-github-actions-the-right-way-4k6m</link>
      <guid>https://dev.to/ptagl/how-to-use-the-cache-with-github-actions-the-right-way-4k6m</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I’ve been using GitHub Actions heavily for CI on my Rust projects lately. I’m always obsessed with increasing execution speed and efficiency, and recently, I thought I had found a clever optimization that turned out to be a huge mistake.&lt;/p&gt;

&lt;p&gt;Here is the context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development strategy&lt;/strong&gt;: I keep a &lt;code&gt;main&lt;/code&gt; branch on which I merge changes through feature branches and pull requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI workflow&lt;/strong&gt;: I usually run a standard lint (&lt;code&gt;cargo clippy&lt;/code&gt; and &lt;code&gt;cargo fmt&lt;/code&gt;), build, and test pipeline. My CI was configured to run on every push to a pull Request. Since I strictly merge PRs via &lt;strong&gt;rebase&lt;/strong&gt; onto &lt;code&gt;main&lt;/code&gt;, the CI run on the PR and the hypothetical run on main after the merge are mathematically identical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, to save on GitHub Action minutes, I had a "brilliant" idea: &lt;strong&gt;I disabled the CI triggers on the &lt;code&gt;main&lt;/code&gt; branch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I thought I was being efficient. In reality, I broke my caching strategy completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mortal Sin: Ignoring Scope
&lt;/h2&gt;

&lt;p&gt;The moment I disabled the build on &lt;code&gt;main&lt;/code&gt;, my cache hit rate immediately dropped to zero. I noticed that every new PR was compiling from scratch, only hitting the cache on subsequent commits &lt;em&gt;within&lt;/em&gt; that PR.&lt;/p&gt;

&lt;p&gt;The reason lies in how GitHub Actions scopes its cache:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Main Branch&lt;/strong&gt;: When CI runs on &lt;code&gt;main&lt;/code&gt;, the cache it saves is accessible to &lt;code&gt;main&lt;/code&gt; and any child branch (future PRs).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PR Branch&lt;/strong&gt;: When CI runs on a PR branch, the cache is saved and scoped &lt;strong&gt;only&lt;/strong&gt; to that specific branch.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once a PR is merged, that branch is deleted. If you don't run CI on &lt;code&gt;main&lt;/code&gt; to regenerate/update the "central" cache, that data is lost to the void. New PRs cannot access the cache generated by previous PRs; they can only access the cache from the base branch (&lt;code&gt;main&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;By disabling CI on &lt;code&gt;main&lt;/code&gt;, I ensured that every new PR started with a cold cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check if you are doing it wrong
&lt;/h2&gt;

&lt;p&gt;You don't need complex metrics. Just look at your CI logs.&lt;/p&gt;

&lt;p&gt;If you are using &lt;code&gt;Swatinem/rust-cache&lt;/code&gt; or &lt;code&gt;sccache&lt;/code&gt;, look at the setup/restore steps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Good&lt;/strong&gt;: The logs explicitly state a cache entry was found and restored.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Run Swatinem/rust-cache@v2
Cache Configuration

... Restoring cache ...
Cache hit &lt;span class="k"&gt;for &lt;/span&gt;restore-key: v0-rust-build-and-test-Linux-x64-xxxxxxxx-xxxxxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;Bad&lt;/strong&gt;: The logs say "No cache found," and it begins downloading the crates registry or compiling dependencies from zero.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Run Swatinem/rust-cache@v2
Cache Configuration

... Restoring cache ...
No cache found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Right Workflow
&lt;/h2&gt;

&lt;p&gt;For my Rust setup, I use &lt;code&gt;Swatinem/rust-cache&lt;/code&gt; (which is excellent for standard cargo registry/target caching) and for heavier projects, I add &lt;code&gt;sccache&lt;/code&gt; to cache the compilation artifacts.&lt;/p&gt;

&lt;p&gt;Here is the corrected workflow. It runs both on PRs &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;main&lt;/code&gt;, preventing the "cold start" problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Rust CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;CARGO_TERM_COLOR&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
  &lt;span class="c1"&gt;# [Optional] Enable sccache for rustc (not needed if you don't want to use `sccache`)&lt;/span&gt;
  &lt;span class="na"&gt;RUSTC_WRAPPER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sccache&lt;/span&gt;
  &lt;span class="c1"&gt;# [Optiona] Tell sccache to use GitHub Actions cache API (not needed if you don't want to use `sccache`)&lt;/span&gt;
  &lt;span class="na"&gt;SCCACHE_GHA_ENABLED&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Test &amp;amp; Lint&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# 1. Checkout&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Git checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;

      &lt;span class="c1"&gt;# 2. Setup the toolchain&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Rust toolchain&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dtolnay/rust-toolchain@master&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rustfmt, clippy&lt;/span&gt;

      &lt;span class="c1"&gt;# 3. Setup Cargo cache (registry + target)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Rust Cache&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Swatinem/rust-cache@v2&lt;/span&gt;

      &lt;span class="c1"&gt;# 4. [Optional] Setup sccache (mainly useful for heavy projects)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run sccache-cache&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mozilla-actions/sccache-action@v0.0.9&lt;/span&gt;

      &lt;span class="c1"&gt;# 5. Check formatting&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cargo fmt&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo fmt --all -- --check&lt;/span&gt;

      &lt;span class="c1"&gt;# 6. Linting&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cargo clippy&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo clippy --all-targets -- -D warnings&lt;/span&gt;

      &lt;span class="c1"&gt;# 7. Build&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cargo build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo build&lt;/span&gt;

      &lt;span class="c1"&gt;# 8. Test&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cargo test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cargo test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Fix Android Battery Drain With Battery Historian</title>
      <dc:creator>Ptagl</dc:creator>
      <pubDate>Sun, 18 Jan 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ptagl/fix-android-battery-drain-with-battery-historian-553a</link>
      <guid>https://dev.to/ptagl/fix-android-battery-drain-with-battery-historian-553a</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When &lt;strong&gt;Android&lt;/strong&gt; starts draining battery with the screen off, the first thing I do is check the battery report in &lt;strong&gt;Settings&lt;/strong&gt; to see the percentage used by each application. Sometimes it's useful, but other times it isn't. Last time I checked, usage looked normal, but at the bottom of the list there was a suspicious entry marked as &lt;code&gt;Other&lt;/code&gt; with a very high percentage. I wanted to know which application or service was contributing to that bucket, but unfortunately it wasn't possible.&lt;/p&gt;

&lt;p&gt;When that happens, the best option is to rely on more powerful tools.&lt;/p&gt;

&lt;p&gt;A tool still worth trying is &lt;a href="https://github.com/google/battery-historian" rel="noopener noreferrer"&gt;Battery Historian&lt;/a&gt;, an inspection tool developed by Google that can analyze and show relevant information from &lt;strong&gt;ADB bugreports&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is no longer maintained, and Google &lt;a href="https://developer.android.com/topic/performance/power/setup-battery-historian" rel="noopener noreferrer"&gt;suggests&lt;/a&gt; to use some alternatives, but &lt;strong&gt;Battery Historian&lt;/strong&gt; is still quite effective and relatively easy to use.&lt;/p&gt;

&lt;p&gt;The project's README provides very easy instructions to run the tool with &lt;strong&gt;Docker&lt;/strong&gt;, but, unfortunately, the Docker image they mention is no longer available.&lt;/p&gt;

&lt;p&gt;Luckily, there are &lt;strong&gt;unofficial images&lt;/strong&gt;, I used the one published on Docker Hub by &lt;a href="https://hub.docker.com/r/itachi1706/battery-historian" rel="noopener noreferrer"&gt;itachi1706&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android Wakeups
&lt;/h2&gt;

&lt;p&gt;We have a problem and also a tool to study it, that's a good starting point. But what are we looking for?&lt;/p&gt;

&lt;p&gt;The crucial concept here is system &lt;strong&gt;wakeups&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In an ideal idle state, when the screen is off, Android tries to keep the device &lt;strong&gt;sleeping&lt;/strong&gt; as much as possible, while still allowing a few vital tasks to run in the background.&lt;br&gt;
​&lt;br&gt;
&lt;strong&gt;Wakeups&lt;/strong&gt; (alarms, jobs, wakelocks) are the mechanism that makes this possible, but if they happen &lt;strong&gt;too frequently&lt;/strong&gt; (especially because of non-essential apps/services) the phone never really stays idle, and battery drain becomes very noticeable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;USB debugging mode enabled on the smartphone&lt;/li&gt;
&lt;li&gt;ADB&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  (1) Enable USB Debugging
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;USB debugging&lt;/code&gt; mode is in the &lt;code&gt;Developer Options&lt;/code&gt; section, which is hidden in the settings and needs to be enabled. To do so, it's typically enough to go to &lt;code&gt;Settings -&amp;gt; System Information&lt;/code&gt; and tap a few times on the Build Number.&lt;/p&gt;

&lt;p&gt;Once enabled, you'll find a lot of settings under the &lt;strong&gt;Developer options&lt;/strong&gt;, among them you need to enable &lt;code&gt;USB Debugging&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  (2) Create a report with ADB
&lt;/h2&gt;

&lt;p&gt;There are a couple of ways of installing ADB, I prefer the manual download in the &lt;a href="https://developer.android.com/tools/releases/platform-tools" rel="noopener noreferrer"&gt;SDK Platform Tools&lt;/a&gt; bundle.&lt;/p&gt;

&lt;p&gt;Once ready, check if it works properly by running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb devices
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output should look like this (if the smartphone is not yet connected to the PC):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;*&lt;/span&gt; daemon not running&lt;span class="p"&gt;;&lt;/span&gt; starting now at tcp:5037
&lt;span class="k"&gt;*&lt;/span&gt; daemon started successfully
List of devices attached
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, &lt;strong&gt;connect&lt;/strong&gt; the smartphone to the PC via &lt;strong&gt;USB&lt;/strong&gt;. Android should show a request to allow the usage of USB debugging on the detected PC. Accept, then run again the &lt;code&gt;devices&lt;/code&gt; ADB command, the output list should now include the smartphone ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;List of devices attached
XXXXXXXXXXXXXXX device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next step is to create the Android &lt;strong&gt;report&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb bugreport bugreport.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a while, the report is ready for the analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;br&gt;
The report contains data for a window of several hours. It may include noise that makes the following inspection harder. In case, it's possible to reset the statistics to have a better control over the monitoring window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell dumpsys batterystats &lt;span class="nt"&gt;--reset&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  (3) Analyze the report with Battery Historian
&lt;/h2&gt;

&lt;p&gt;Run the Battery Historian container with &lt;strong&gt;Docker&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 9999:9999 itachi1706/battery-historian:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open the browser and go to &lt;a href="http://localhost:9999/" rel="noopener noreferrer"&gt;http://localhost:9999/&lt;/a&gt;, select the report archive created with ADB and press the &lt;code&gt;Submit&lt;/code&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzc3uqm8qsyns60tz46l9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzc3uqm8qsyns60tz46l9.png" alt="Battery Historian Timeline" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The UI includes a &lt;strong&gt;timeline chart&lt;/strong&gt; showing events and metrics about system services and components. Below there is a &lt;strong&gt;table&lt;/strong&gt; with all the details, and the most interesting view is often &lt;code&gt;App Wakeup Alarms&lt;/code&gt;. It shows all the services that triggered a system &lt;strong&gt;wakeup&lt;/strong&gt;. Some services are required and it's normal to find them in the rankings (like &lt;code&gt;ANDROID_SYSTEM&lt;/code&gt;, &lt;code&gt;GOOGLE_SERVICES&lt;/code&gt;, &lt;code&gt;SYSTEM_UI&lt;/code&gt;, etc.), but there may be something unexpected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6nfkr2fmzff3qudrtzm7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6nfkr2fmzff3qudrtzm7.png" alt="Battery Historian Wakeups" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my case, the top drainer was the &lt;strong&gt;Samsung Health&lt;/strong&gt; app, got it!&lt;/p&gt;

&lt;p&gt;A solution could be completely uninstalling the application, but I needed it, so I better find the root cause. The good news is that it's possible to &lt;strong&gt;break down&lt;/strong&gt; the standings by "alarm name" through the checkbox immediately above the table, providing way more details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz8j568r808m1dw98j88.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz8j568r808m1dw98j88.png" alt="Battery Historian Wakeups Breakdown" width="800" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Do you see? It is the background service responsible for the daily &lt;strong&gt;step counter&lt;/strong&gt;. I'm very lucky this time: I don't need my smartphone taking care of this activity because I already have a smartwatch for that purpose, so I can just disable that service from the application settings. To be even more sure, I completely removed any app permission related to "Physical Activity".&lt;/p&gt;

&lt;p&gt;After this countermeasure, I didn't experience any battery drain anymore!&lt;/p&gt;

</description>
      <category>android</category>
    </item>
    <item>
      <title>Dev Containers: Rust Quick Start</title>
      <dc:creator>Ptagl</dc:creator>
      <pubDate>Fri, 16 Jan 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ptagl/dev-containers-rust-quick-start-4g2m</link>
      <guid>https://dev.to/ptagl/dev-containers-rust-quick-start-4g2m</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Dev Containers let you use Docker to run an isolated development environment, so that your tooling and runtime stack can live “somewhere else” instead of on your host machine.&lt;br&gt;
​&lt;br&gt;
A &lt;code&gt;devcontainer.json&lt;/code&gt; file in your repo defines how to create or connect to that development container (and which tools/extensions/settings to include).&lt;br&gt;
​&lt;br&gt;
Dev Containers have become pretty mainstream lately, and I’m finding them especially useful in several scenarios, for instance when I need to temporarily set up a development environment or a specific toolchain for a quick test I'll later discard, and I don't want to pollute my host machine.&lt;/p&gt;
&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;To follow this tutorial, you need to have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;Visual Studio Code with the "Dev Containers" extension installed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tested this on Ubuntu 24.04 on WSL2 (Windows 11), but it should work on any platform supported by Docker and VS Code, with minor adjustments.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding the Dev Container configuration
&lt;/h2&gt;

&lt;p&gt;First of all, let's create our project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;hello-dev-container &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;hello-dev-container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, let's create a file called &lt;code&gt;.devcontainer/devcontainer.json&lt;/code&gt; in the project root with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rust Hello World (Dev Container)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcr.microsoft.com/devcontainers/rust:latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customizations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vscode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"extensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rust-lang.rust-analyzer"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you haven't done it yet, open the project root folder with VS Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use the Command Palette (&lt;code&gt;Ctrl&lt;/code&gt; + &lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;P&lt;/code&gt;), and select &lt;code&gt;Dev Containers: Reopen in Container&lt;/code&gt;. This command may take a while the first time as Docker needs to download the base image and set up the container. Once finished, the blue bottom-left corner of VS Code should indicate that you are connected to the Dev Container. As an additional confirmation, you can open a terminal in VS Code and see that the environment looks different from your host machine (e.g., the current user is &lt;code&gt;vscode&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;At this point, you can continue working as usual; the fact that you are inside a Dev Container is mostly transparent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hello World Rust Template
&lt;/h2&gt;

&lt;p&gt;To test the Dev Container with Rust, we need at least a minimal project that can be compiled, run, and eventually debugged. To do so, let's open a VS Code terminal and type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo init &lt;span class="nt"&gt;--bin&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; hello-dev-container
cargo run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the "Hello, world!" string printed to the terminal. The same could be done directly from VS Code by clicking on the buttons on top of the &lt;code&gt;main()&lt;/code&gt; function. Debugging works as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the environment isolation
&lt;/h2&gt;

&lt;p&gt;To verify that the Dev Container is indeed isolated from your host machine, let's suppose that we want to test our "Hello World" Rust application with the latest nightly toolchain. We could install it on our host machine, but maybe we won't need it beyond this quick test, and we are too lazy to uninstall it later. Instead, we can simply install it inside the Dev Container.&lt;/p&gt;

&lt;p&gt;To force &lt;code&gt;cargo&lt;/code&gt; to use a specific toolchain version, let's create a file called &lt;code&gt;rust-toolchain.toml&lt;/code&gt; in the project root with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[toolchain]
channel = "nightly-2026-01-15"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VS Code should automatically detect the change and attempt to refresh the project, but the Rust Analyzer extension may fail as the specified toolchain is not yet installed inside the Dev Container.&lt;br&gt;
If you open the extension log, you should see an error like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;toolchain 'nightly-2026-01-15-x86_64-unknown-linux-gnu' is not installed
help: run `rustup toolchain install` to install it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's accept the suggestion and run the command in the VS Code terminal inside the Dev Container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rustup toolchain &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once finished, you may need to restart Rust Analyzer, and then the IDE is fully functional again. You can notice, for instance, that we can run or debug the application by clicking on the buttons on top of the &lt;code&gt;main()&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Since I prefer to verify rather than trust, let's make a quick check. From the IDE terminal, run &lt;code&gt;rustup show&lt;/code&gt;. It lists the toolchains installed on the Dev Container, and should include &lt;code&gt;nightly-2026-01-15&lt;/code&gt;. Now, if you have Rustup installed on the host machine, you can do the same and compare the output: that toolchain version should not appear unless you installed it on your own, regardless of this tutorial.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>devcontainers</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Claiming WSL Disk Space</title>
      <dc:creator>Ptagl</dc:creator>
      <pubDate>Sun, 11 Jan 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ptagl/claiming-wsl-disk-space-eb2</link>
      <guid>https://dev.to/ptagl/claiming-wsl-disk-space-eb2</guid>
      <description>&lt;h2&gt;
  
  
  TLDR
&lt;/h2&gt;

&lt;p&gt;Shut down your WSL distributions and use PowerShell (as administrator) to run &lt;code&gt;Optimize-VHD&lt;/code&gt; on the VHDX file used by your WSL distribution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--shutdown&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Optimize-VHD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;Path to your VHDX file&amp;gt;'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Full&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Working with WSL I sometimes see my disk usage grow significantly until the day I'm almost out of space and need to reclaim it somehow. In my case, the reason is that WSL uses a virtual hard disk (VHDX) file to store the file system of each distribution, and over time these files grow in size as data is added, but they don't shrink when data is removed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: this is true unless your WSL distribution is configured to use the "sparse mode", which should automatically resize the VHDX file as needed. However, many people reported issues with this mode (for instance &lt;a href="https://github.com/microsoft/WSL/issues/10703" rel="noopener noreferrer"&gt;here&lt;/a&gt;, &lt;a href="https://github.com/microsoft/WSL/issues/12103" rel="noopener noreferrer"&gt;here&lt;/a&gt;, and &lt;a href="https://github.com/microsoft/WSL/issues/4699#issuecomment-565700099" rel="noopener noreferrer"&gt;here&lt;/a&gt;), so it's not considered a reliable solution as of today.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It’s easy to check whether you have this issue too. Since I always forget the path of my WSL VHDX file, I typically use PowerShell to find it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERPROFILE&lt;/span&gt;&lt;span class="nx"&gt;\AppData\Local\Packages\&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.vhdx"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the path and store it in a variable for convenience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$VHDXPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;Path to your VHDX file&amp;gt;'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, check the size of the VHDX file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"{0:N2} GB"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;Get-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$VHDXPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1GB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, it's time to compare this size with the actual used space inside the WSL distribution. To do this, start your WSL distribution and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the size of the VHDX file is significantly larger than the used space reported by &lt;code&gt;df&lt;/code&gt;, then it's definitely time to reclaim some bytes!&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize-VHD to the rescue
&lt;/h2&gt;

&lt;p&gt;Luckily, there is a PowerShell cmdlet called &lt;a href="https://learn.microsoft.com/en-us/powershell/module/hyper-v/optimize-vhd?view=windowsserver2025-ps" rel="noopener noreferrer"&gt;Optimize-VHD&lt;/a&gt; that can be used to compact VHDX files. This command reclaims unused space in the VHDX file, effectively reducing its size on disk.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;code&gt;Optimize-VHD&lt;/code&gt; is part of the Hyper-V module. Availability on your system depends on your Windows edition and features installed. If you don't have it, you might need to enable the Hyper-V feature on your Windows system via "Turn Windows features on or off" in the Control Panel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First of all, make sure the VHDX file is not mounted (the easiest way is to stop WSL), then run the &lt;code&gt;Optimize-VHD&lt;/code&gt; command as administrator on the VHDX file you found earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--shutdown&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Optimize-VHD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$VHDXPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Full&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process may take some time depending on the size of the VHDX file and the amount of unused space to reclaim.&lt;/p&gt;

&lt;p&gt;After the command completes, you can check the size of the VHDX file again with the same commands used above to see how much space you've reclaimed.&lt;/p&gt;

&lt;p&gt;And that's all, see you next time your disk is full!&lt;/p&gt;

</description>
      <category>wsl</category>
    </item>
  </channel>
</rss>
