<?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: Tony Rowan</title>
    <description>The latest articles on DEV Community by Tony Rowan (@tonyrowan).</description>
    <link>https://dev.to/tonyrowan</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%2F880998%2F9381d393-69bc-4956-ac68-631156c2e7d5.jpeg</url>
      <title>DEV Community: Tony Rowan</title>
      <link>https://dev.to/tonyrowan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tonyrowan"/>
    <language>en</language>
    <item>
      <title>Mock User Agent in View Component Specs</title>
      <dc:creator>Tony Rowan</dc:creator>
      <pubDate>Fri, 17 Jan 2025 11:26:32 +0000</pubDate>
      <link>https://dev.to/tonyrowan/mock-user-agent-in-view-component-specs-1fhf</link>
      <guid>https://dev.to/tonyrowan/mock-user-agent-in-view-component-specs-1fhf</guid>
      <description>&lt;p&gt;Recently, I implemented a couple of ViewComponents to display links to our app in the app stores. These ViewComponents contained in a single place: the appropriate logo for app store, the link to the app in said app store, descriptions to make the links accessible and click tracking.&lt;/p&gt;

&lt;p&gt;The links also needed to support a small piece of behaviour. We needed the links to only be displayed on what we deemed an appropriate platform. Explicitly, the link to the Apple App Store shouldn't be rendered on Android devices and the link to the Google Play Store shouldn't be rendered on iOS devices.&lt;/p&gt;

&lt;p&gt;To keep things very simple and self-contained, we can use the &lt;a href="https://github.com/fnando/browser" rel="noopener noreferrer"&gt;browser gem&lt;/a&gt; and access the view helper it provides directly from the view component templates to detect in the component should render anything.&lt;/p&gt;

&lt;p&gt;The resulting view components look a little something like this. Descriptions and click tracking have been omitted for brevity, we're not looking at those in this post.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/components/apple_app_store_link_component.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppleAppStoreLinkComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/components/apple_app_store_link_component.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;android?&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;link_to_the_app_on_the_apple_app_store&amp;gt;"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;image_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"apple_app_store.svg"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Testing these components should be simple. We should be able to set the User Agent String to match an android device and an ios device and assert that the component renders (or not) as appropriate.&lt;/p&gt;

&lt;p&gt;We're using RSpec and we already have &lt;code&gt;:android&lt;/code&gt; and &lt;code&gt;:ios&lt;/code&gt; tags that are used in system test hooks to set the relevant User Agent String on the Rack driver.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;android: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"User-Agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;android_user_agent_string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ios: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"User-Agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ios_user_agent_string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Obviously these hooks would not work as-is, we need to also target component tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;android: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"User-Agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;android_user_agent_string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ios: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"User-Agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ios_user_agent_string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can write our simple and descriptive component specs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;RSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt; &lt;span class="no"&gt;AppleAppStoreLinkComponent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :component&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"does not render the link on Android devices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:android&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;render_inline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;described_class&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="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;not_to&lt;/span&gt; &lt;span class="n"&gt;have_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apple_app_store_selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s2"&gt;"renders the link on iOS devices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:ios&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;render_inline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;described_class&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="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;have_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apple_app_store_selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apple_app_store_selector&lt;/span&gt;
    &lt;span class="s2"&gt;"a[href='&amp;lt;link_to_the_app_on_the_apple_app_store&amp;gt;'] img[src*='apple_app_store']"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, this doesn't work. The Capybara driver used in component specs is very simple and doesn't support the full API supported by, say, the Rack driver. Here, we get an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  NoMethodError:
    undefined method `driver' for an instance of Capybara::Node::Simple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need another way to set the User Agent String for component tests.&lt;/p&gt;

&lt;p&gt;Looking through the ViewComponent &lt;a href="https://viewcomponent.org/guide/testing.html" rel="noopener noreferrer"&gt;Testing&lt;/a&gt; guide didn't help. There were sections on setting the current request parameters, but nothing about how to set&lt;br&gt;
arbitrary headers. GPT and Copilot were also of no help, since they just hallucinated non-existent APIs.&lt;/p&gt;

&lt;p&gt;Before giving up and using a full-on system test for this behaviour, which felt very heavy for a simple case of user agent detection, I looked through the extensive &lt;a href="https://viewcomponent.org/api.html" rel="noopener noreferrer"&gt;API documentation&lt;/a&gt;. I found &lt;a href="https://viewcomponent.org/api.html#vc_test_request--actiondispatchtestrequest" rel="noopener noreferrer"&gt;&lt;code&gt;vc_test_request&lt;/code&gt;&lt;/a&gt;, a way to access and modify the request object directly in tests - the docs even used the User Agent header as an example.&lt;/p&gt;

&lt;p&gt;This was perfect! I plugged it into the hook and everything worked as expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;android: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;vc_test_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HTTP_USER_AGENT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;android_user_agent_string&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:each&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ios: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;vc_test_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"HTTP_USER_AGENT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ios_user_agent_string&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote this post because it wasn't immediately clear to me from the documentation that this testing behaviour was available and I thought maybe others would find it useful.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@brrknees?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;berenice melis&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/gray-concrete-building-Un2hYOi1np0?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>viewcomponent</category>
      <category>rspec</category>
    </item>
    <item>
      <title>How To: Implement an Android Smart App Banner</title>
      <dc:creator>Tony Rowan</dc:creator>
      <pubDate>Fri, 23 Jun 2023 14:30:26 +0000</pubDate>
      <link>https://dev.to/tonyrowan/how-to-implement-an-android-smart-app-banner-55e7</link>
      <guid>https://dev.to/tonyrowan/how-to-implement-an-android-smart-app-banner-55e7</guid>
      <description>&lt;p&gt;So there isn't an Android Smart App Banner like the one &lt;a href="https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners" rel="noopener noreferrer"&gt;for iOS&lt;/a&gt; but there &lt;em&gt;is&lt;/em&gt; the &lt;a href="https://developer.chrome.com/blog/app-install-banners-native" rel="noopener noreferrer"&gt;Native App Install Prompt&lt;/a&gt;, which is about as much as we get natively on Android. Sorry for the bait-and-switch, there...&lt;/p&gt;

&lt;p&gt;It looks like this.&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%2Fpt20ryvdscizmnny2qbg.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%2Fpt20ryvdscizmnny2qbg.png" alt="Native App Install Prompt Banner" width="800" height="988"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tapping the banner brings up a dialog like this.&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%2Fneqg26d7gfhkvv2b193r.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%2Fneqg26d7gfhkvv2b193r.png" alt="Native App Install Prompt Dialog" width="800" height="991"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tapping install in that dialog takes you to the Play Store.&lt;/p&gt;

&lt;h3&gt;
  
  
  So, How Do You Implement It?
&lt;/h3&gt;

&lt;p&gt;Unfortunately, the documentation for the prompt isn't very good. It skips over important information and talks about a very similar prompt for PWAs without ever clarifying which bits matter to what prompts.&lt;/p&gt;

&lt;p&gt;To get this banner working for Native Android Apps all you have to do is add a manifest file with the following properties: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;short_name&lt;/code&gt;: The name of your app/brand/company/whatever. It's shown on the initial banner.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt;: A longer form name for your app/brand/company/whatever. It's shown on the dialog.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;icons&lt;/code&gt;: An array of objects describing the icons to show on the banner and dialog. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"prefer_related_applications": true&lt;/code&gt;. If this key is missing or not set to &lt;code&gt;true&lt;/code&gt;, the banner won't show up.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;related_applications&lt;/code&gt;: A list of objects that describe the app to get the user to install. To get the banner to show up on Android, the list must contain an object with a key called &lt;code&gt;platform&lt;/code&gt; set to &lt;code&gt;play&lt;/code&gt; and a key called &lt;code&gt;id&lt;/code&gt;, which is set to the ID of the app in the Play Store.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You then have to ensure that the manifest is declared in your HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"manifest"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/manifest.json"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it, the prompt will now show up. You don't need to listen for events or show buttons or anything. I repeat:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All you need to do to get the banner to show up is to add a manifest file with the required properties and declare it in your HTML.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&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;"short_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;"Cool App"&lt;/span&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;"Cool App - You're going to love it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icons"&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="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/android-chrome-192x192.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192x192"&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="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/android-chrome-512x512.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sizes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512x512"&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="nl"&gt;"prefer_related_applications"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"related_applications"&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="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"play"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.example.cool_app"&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;h3&gt;
  
  
  I Don't See the Prompt
&lt;/h3&gt;

&lt;p&gt;Unfortunately, even if you do this, the prompt still will not likely show up. As noted in the documentation, there are several other factors that Chrome uses before it will trigger the prompt. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user needs to not have the app already installed (obviously...otherwise you'd just add a static banner in your HTML, right?)&lt;/li&gt;
&lt;li&gt;The page must be served over HTTPS (I hope your production traffic is already doing this)&lt;/li&gt;
&lt;li&gt;The user needs to be on a mobile device. I haven't seen this listed anywhere, but I've only ever gotten it to trigger using mobile Chrome. It feels kind of obvious when you think about it.&lt;/li&gt;
&lt;li&gt;The user needs to meet some kind of interaction requirement. This is pretty vague and basically means the prompt won't show up the first time (probably not the second or third time either) the user visits your site. They will only see it if they are interacting with the domain quite often.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These requirements can make testing whether you've set up your manifest correctly quite difficult. Fortunately, there are a couple of &lt;a href="https://dev.tochrome://flags"&gt;Chrome flags&lt;/a&gt; you can use to make things easier.&lt;/p&gt;

&lt;h4&gt;
  
  
  Bypass User Engagement Checks
&lt;/h4&gt;

&lt;p&gt;If you enable #bypass-app-banner-engagement-checks, then the banner will show up on the first (and every) visit to your site, as long as the other criteria are met.&lt;/p&gt;

&lt;h4&gt;
  
  
  Insecure Origins Treated As Secure
&lt;/h4&gt;

&lt;p&gt;Since you need to be on a mobile device, it might be hard to visit your site running in local development mode without paying for a service like &lt;a href="https://ngrok.com/" rel="noopener noreferrer"&gt;ngrok&lt;/a&gt;. On Mac OS, at least, you can visit your site running locally from a mobile device on the same WiFi network by simply visiting the IP for your Mac (found in the wifi settings panel). You can then tell Chrome to treat that IP address as secure by adding it to the #unsafely-treat-insecure-origin-as-secure flag.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced Usage
&lt;/h3&gt;

&lt;p&gt;You don't have to serve your manifest file as a static file. If you want to make use of asset helpers or translations, you can back your manifest file with an endpoint. This example is ruby based, but the same approach works wherever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"manifest"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/webapp_manifest"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# routes.rb&lt;/span&gt;
&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="ss"&gt;:webapp_manifest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: :show&lt;/span&gt;

&lt;span class="c1"&gt;# app/controllers/webapp_manifests_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebappManifestsController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"long_app_name"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;short_name: &lt;/span&gt;&lt;span class="no"&gt;I18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"short_app_name"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;prefer_related_applications: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;related_applications: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="no"&gt;AppIds&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PLAY_STORE_APP_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;platform: &lt;/span&gt;&lt;span class="s2"&gt;"play"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;icons: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;src: &lt;/span&gt;&lt;span class="n"&gt;asset_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app_store_icon_512"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;sizes: &lt;/span&gt;&lt;span class="s2"&gt;"512x512"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;        
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;src: &lt;/span&gt;&lt;span class="n"&gt;asset_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"app_store_icon_192"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;sizes: &lt;/span&gt;&lt;span class="s2"&gt;"192x192"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;I hope you find this post useful and less confusing than the official documentation. &lt;/p&gt;

&lt;p&gt;I've now implemented this for three different sites for three different companies over the last 5 years and each time I wished for a post like this.&lt;/p&gt;

&lt;p&gt;So, this time, I wrote it down. This has mainly been for me more than anything 😉&lt;/p&gt;




&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@towfiqu999999?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Towfiqu barbhuiya&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/5xTYgw2g7aw?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>android</category>
    </item>
    <item>
      <title>A Deep Dive into Memory Leaks in Ruby</title>
      <dc:creator>Tony Rowan</dc:creator>
      <pubDate>Wed, 17 Aug 2022 11:28:09 +0000</pubDate>
      <link>https://dev.to/appsignal/a-deep-dive-into-memory-leaks-in-ruby-3bc7</link>
      <guid>https://dev.to/appsignal/a-deep-dive-into-memory-leaks-in-ruby-3bc7</guid>
      <description>&lt;p&gt;In the &lt;a href="https://blog.appsignal.com/2022/07/27/how-to-track-down-memory-leaks-in-ruby.html" rel="noopener noreferrer"&gt;first part of this two-part series on memory leaks&lt;/a&gt;, we looked at how Ruby manages memory and how Garbage Collection (GC) works.&lt;/p&gt;

&lt;p&gt;You might be able to afford powerful machines with more memory, and your app might restart often enough that your users don't notice, but memory usage matters.&lt;/p&gt;

&lt;p&gt;Allocation and Garbage Collection aren't free. If you have a leak, you spend more and more time on Garbage Collection instead of doing what you built your app to do.&lt;/p&gt;

&lt;p&gt;In this post, we'll look deeper into the tools you can use to discover and diagnose a memory leak.&lt;/p&gt;

&lt;p&gt;Let's continue!&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding Leaks in Ruby
&lt;/h2&gt;

&lt;p&gt;Detecting a leak is simple enough. You can use &lt;code&gt;GC&lt;/code&gt;, &lt;code&gt;ObjectSpace&lt;/code&gt;, and the RSS graphs in your APM tool to watch your memory usage increase. But just knowing you have a leak is not enough to fix it. You need to know where it is coming from. Raw numbers can't tell you that.&lt;/p&gt;

&lt;p&gt;Fortunately, the Ruby ecosystem has some great tools to attach context to those numbers. Two are &lt;code&gt;memory-profiler&lt;/code&gt; and &lt;code&gt;derailed_benchmarks&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;memory_profiler&lt;/code&gt; in Ruby
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/SamSaffron/memory_profiler" rel="noopener noreferrer"&gt;&lt;code&gt;memory_profiler&lt;/code&gt; gem&lt;/a&gt; offers a very simple API and a detailed (albeit a little overwhelming) allocated and retained memory report — that includes the classes of objects that are allocated, their size, and where they were allocated. It's straightforward to add to our leaky program.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# leaky.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"memory_profiler"&lt;/span&gt;

&lt;span class="n"&gt;an_array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;MemoryProfiler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;

    &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;an_array&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"B"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"C"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Array is &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;an_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; items long"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="no"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pretty_print&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outputting a report that looks similar to this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Total allocated: 440072 bytes &lt;span class="o"&gt;(&lt;/span&gt;11001 objects&lt;span class="o"&gt;)&lt;/span&gt;
Total retained:  440072 bytes &lt;span class="o"&gt;(&lt;/span&gt;11001 objects&lt;span class="o"&gt;)&lt;/span&gt;

allocated memory by gem
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
    440072  other

allocated memory by file
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
    440072  ./leaky.rb

allocated memory by location
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
    440000  ./leaky.rb:9
        72  ./leaky.rb:10

allocated memory by class
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
    440000  String
        72  Thread::Mutex

allocated objects by gem
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     11001  other

allocated objects by file
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     11001  ./leaky.rb

allocated objects by location
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     11000  ./leaky.rb:9
         1  ./leaky.rb:10

allocated objects by class
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     11000  String
         1  Thread::Mutex

retained memory by gem
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
    440072  other

retained memory by file
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
    440072  ./leaky.rb

retained memory by location
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
    440000  ./leaky.rb:9
        72  ./leaky.rb:10

retained memory by class
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
    440000  String
        72  Thread::Mutex

retained objects by gem
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     11001  other

retained objects by file
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     11001  ./leaky.rb

retained objects by location
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     11000  ./leaky.rb:9
         1  ./leaky.rb:10

retained objects by class
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     11000  String
         1  Thread::Mutex


Allocated String Report
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     11000  &lt;span class="s2"&gt;"ABC"&lt;/span&gt;
     11000  ./leaky.rb:9


Retained String Report
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     11000  &lt;span class="s2"&gt;"ABC"&lt;/span&gt;
     11000  ./leaky.rb:9

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

&lt;/div&gt;



&lt;p&gt;There is a lot of information here, but generally, the&lt;br&gt;
&lt;code&gt;allocated objects by location&lt;/code&gt; and &lt;code&gt;retained objects by location&lt;/code&gt; sections can be the most useful when looking for leaks. These are the file locations that allocate objects, ordered by the number of allocated objects.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;allocated&lt;/code&gt;&lt;/strong&gt; objects are all objects allocated (created) within the &lt;code&gt;report&lt;/code&gt; block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;retained&lt;/code&gt;&lt;/strong&gt; objects are objects that have not been garbage collected by the end of the &lt;code&gt;report&lt;/code&gt; block. We forced a GC run before the end of the block so we could see the leaked objects more clearly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be careful about trusting the &lt;code&gt;retained&lt;/code&gt; object counts. They depend heavily on what portion of the leaking code is within the &lt;code&gt;report&lt;/code&gt; block.&lt;/p&gt;

&lt;p&gt;For example, if we move the declaration of &lt;code&gt;an_array&lt;/code&gt; into the &lt;code&gt;report&lt;/code&gt; block, we might be fooled into thinking the code isn't leaky.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# leaky.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"memory_profiler"&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;MemoryProfiler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;an_array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;

    &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;an_array&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"B"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"C"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Array is &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;an_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; items long"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="no"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pretty_print&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The top of the resulting report won't report many retained objects (just the report itself).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Total allocated: 529784 bytes &lt;span class="o"&gt;(&lt;/span&gt;11002 objects&lt;span class="o"&gt;)&lt;/span&gt;
Total retained:  72 bytes &lt;span class="o"&gt;(&lt;/span&gt;1 objects&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;derailed_benchmarks&lt;/code&gt; in Ruby
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/zombocom/derailed_benchmarks" rel="noopener noreferrer"&gt;&lt;code&gt;derailed_benchmarks&lt;/code&gt; gem&lt;/a&gt; is a suite of very useful tools for all kinds of performance work, primarily aimed at Rails apps. For finding leaks, we want to look at &lt;code&gt;perf:mem_over_time&lt;/code&gt;, &lt;code&gt;perf:objects&lt;/code&gt;, and &lt;code&gt;perf:heap_diff&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These tasks work by sending &lt;code&gt;curl&lt;/code&gt; requests to a running app, so we can't add them to our little leaky program. Instead, we'll need to set up a small Rails app with an endpoint that leaks memory, then install the &lt;code&gt;derailed_benchmarks&lt;/code&gt; on that app.&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="c"&gt;# Create a rails app with no database&lt;/span&gt;
rails new leaky &lt;span class="nt"&gt;--skip-active-record&lt;/span&gt; &lt;span class="nt"&gt;--minimal&lt;/span&gt;

&lt;span class="c"&gt;## Add derailed benchmarks&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;leaky
bundle add derailed_benchmarks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="s2"&gt;"leaks#index"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# app/controllers/leaks_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeaksController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vg"&gt;$an_array&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"B"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"C"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;plain: &lt;/span&gt;&lt;span class="s2"&gt;"Array is &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vg"&gt;$an_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; items long"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# config/initializers/array.rb&lt;/span&gt;
&lt;span class="vg"&gt;$an_array&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;You should now be able to boot the app with &lt;code&gt;bin/rails s&lt;/code&gt;. You'll be able to &lt;code&gt;curl&lt;/code&gt; an endpoint that leaks on each request.&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="nv"&gt;$ &lt;/span&gt;curl http://localhost:3000

Array is 1000 items long

&lt;span class="nv"&gt;$ &lt;/span&gt;curl http://localhost:3000

Array is 2000 items long
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can now use &lt;code&gt;derailed_benchmarks&lt;/code&gt; to see our leak in action.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;perf:mem_over_time&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;This will show us memory use over time (similarly to how we watched the memory growth of our leaky script with &lt;code&gt;watch&lt;/code&gt; and &lt;code&gt;ps&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Derailed will boot the app in production mode, repeatedly hit an endpoint (&lt;code&gt;/&lt;/code&gt; by default), and report the memory usage. If it never stops growing, we have a leak!&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="nv"&gt;$ TEST_COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10000 &lt;span class="nv"&gt;DERAILED_SKIP_ACTIVE_RECORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  bundle &lt;span class="nb"&gt;exec &lt;/span&gt;derailed &lt;span class="nb"&gt;exec &lt;/span&gt;perf:mem_over_time

Booting: production
Endpoint: &lt;span class="s2"&gt;"/"&lt;/span&gt;
PID: 4417
104.33984375
300.609375
455.578125
642.69140625
751.6953125
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: Derailed will boot the Rails app in production mode to perform the tests. By default, it will also &lt;code&gt;require rails/all&lt;/code&gt; first. Since we don't have a database in this app, we need to override this behavior with &lt;code&gt;DERAILED_SKIP_ACTIVE_RECORD=true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can run this benchmark against different endpoints to see which one/s (if any) leak.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;perf:objects&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;perf:objects&lt;/code&gt; task uses &lt;code&gt;memory_profiler&lt;/code&gt; under the hood so the produced report will look familiar.&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="nv"&gt;$ TEST_COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="nv"&gt;DERAILED_SKIP_ACTIVE_RECORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  bundle &lt;span class="nb"&gt;exec &lt;/span&gt;derailed &lt;span class="nb"&gt;exec &lt;/span&gt;perf:objects

Booting: production
Endpoint: &lt;span class="s2"&gt;"/"&lt;/span&gt;
Running 10 &lt;span class="nb"&gt;times
&lt;/span&gt;Total allocated: 2413560 bytes &lt;span class="o"&gt;(&lt;/span&gt;55476 objects&lt;span class="o"&gt;)&lt;/span&gt;
Total retained:  400000 bytes &lt;span class="o"&gt;(&lt;/span&gt;10000 objects&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# The rest of the report...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This report can help narrow down where your leaked memory is being allocated. In our example, the report's last section — the &lt;code&gt;Retained String Report&lt;/code&gt; — tells us exactly what our problem is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Retained String Report
&lt;span class="nt"&gt;-----------------------------------&lt;/span&gt;
     10000  &lt;span class="s2"&gt;"ABC"&lt;/span&gt;
     10000  /Users/tonyrowan/playground/leaky/app/controllers/leaks_controller.rb:3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've leaked 10,000 strings containing "ABC" from the &lt;code&gt;LeaksController&lt;/code&gt; on line 3. In a non-trivial app, this report would be significantly larger and contain retained strings that you want to retain — query caches, etc. — but this and the other 'by location' sections should help you narrow down your leak.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;perf:heap_diff&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;perf:heap_diff&lt;/code&gt; benchmark can help if the report from &lt;code&gt;perf:objects&lt;/code&gt; is too complex to see where your leak is coming from.&lt;/p&gt;

&lt;p&gt;As the name suggests, &lt;code&gt;perf:heap_diff&lt;/code&gt; produces three heap dumps and calculates the difference between them. It creates a report that includes the types of objects retained between dumps and the location that allocated them.&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="nv"&gt;$ DERAILED_SKIP_ACTIVE_RECORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;bundle &lt;span class="nb"&gt;exec &lt;/span&gt;derailed &lt;span class="nb"&gt;exec &lt;/span&gt;perf:heap_diff

Booting: production
Endpoint: &lt;span class="s2"&gt;"/"&lt;/span&gt;
Running 1000 &lt;span class="nb"&gt;times
&lt;/span&gt;Heap file generated: &lt;span class="s2"&gt;"tmp/2022-06-15T11:08:28+01:00-heap-0.ndjson"&lt;/span&gt;
Running 1000 &lt;span class="nb"&gt;times
&lt;/span&gt;Heap file generated: &lt;span class="s2"&gt;"tmp/2022-06-15T11:08:28+01:00-heap-1.ndjson"&lt;/span&gt;
Running 1000 &lt;span class="nb"&gt;times
&lt;/span&gt;Heap file generated: &lt;span class="s2"&gt;"tmp/2022-06-15T11:08:28+01:00-heap-2.ndjson"&lt;/span&gt;

Diff
&lt;span class="o"&gt;====&lt;/span&gt;
Retained STRING 999991 objects of size 39999640/40008500 &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in &lt;/span&gt;bytes&lt;span class="o"&gt;)&lt;/span&gt; at: /Users/tonyrowan/playground/leaky/app/controllers/leaks_controller.rb:3
Retained STRING 2 objects of size 148/40008500 &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in &lt;/span&gt;bytes&lt;span class="o"&gt;)&lt;/span&gt; at: /Users/tonyrowan/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/derailed_benchmarks-2.1.1/lib/derailed_benchmarks/tasks.rb:265
Retained STRING 1 objects of size 88/40008500 &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in &lt;/span&gt;bytes&lt;span class="o"&gt;)&lt;/span&gt; at: /Users/tonyrowan/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/derailed_benchmarks-2.1.1/lib/derailed_benchmarks/tasks.rb:266
Retained DATA 1 objects of size 72/40008500 &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in &lt;/span&gt;bytes&lt;span class="o"&gt;)&lt;/span&gt; at: /Users/tonyrowan/.asdf/installs/ruby/3.1.2/lib/ruby/3.1.0/objspace.rb:87
Retained IMEMO 1 objects of size 40/40008500 &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in &lt;/span&gt;bytes&lt;span class="o"&gt;)&lt;/span&gt; at: /Users/tonyrowan/.asdf/installs/ruby/3.1.2/lib/ruby/3.1.0/objspace.rb:88
Retained IMEMO 1 objects of size 40/40008500 &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in &lt;/span&gt;bytes&lt;span class="o"&gt;)&lt;/span&gt; at: /Users/tonyrowan/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/derailed_benchmarks-2.1.1/lib/derailed_benchmarks/tasks.rb:259
Retained IMEMO 1 objects of size 40/40008500 &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in &lt;/span&gt;bytes&lt;span class="o"&gt;)&lt;/span&gt; at: /Users/tonyrowan/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/derailed_benchmarks-2.1.1/lib/derailed_benchmarks/tasks.rb:260
Retained FILE 1 objects of size 8432/40008500 &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;in &lt;/span&gt;bytes&lt;span class="o"&gt;)&lt;/span&gt; at: /Users/tonyrowan/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/derailed_benchmarks-2.1.1/lib/derailed_benchmarks/tasks.rb:266

Run &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;heapy &lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;more options
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also read &lt;em&gt;&lt;a href="https://medium.com/klaxit-techblog/tracking-a-ruby-memory-leak-in-2021-9eb56575f731#875b" rel="noopener noreferrer"&gt;Tracking a Ruby memory leak in 2021&lt;/a&gt;&lt;/em&gt; to understand better what's going on.&lt;/p&gt;

&lt;p&gt;The report points us exactly where we need to go for our leaky baby app. At the top of the diff, we see 999991 retained string objects allocated from the &lt;code&gt;LeaksController&lt;/code&gt; on line 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaks in Real Ruby and Rails Apps
&lt;/h2&gt;

&lt;p&gt;Hopefully, the examples we've used so far have never been put into real-life apps — I hope no one intends to leak memory!&lt;/p&gt;

&lt;p&gt;In non-trivial apps, memory leaks can be much harder to track down. Retained objects are not always bad — a cache with garbage collected items would not be of much use.&lt;/p&gt;

&lt;p&gt;There is something common between all leaks, though. Somewhere, a root-level object (a class/global, etc.) holds a reference to an object.&lt;/p&gt;

&lt;p&gt;One common example is a cache without a limit or an eviction policy. By definition, this will leak memory since every object put into the cache will remain forever. Over time, this cache will occupy more and more of the memory of an app, with a smaller and smaller percentage of it actually in use.&lt;/p&gt;

&lt;p&gt;Consider the following code that fetches a high score for a game. It's similar to something I've seen in the past. This is an expensive request, and we can easily bust the cache when it changes, so we want to cache it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationModel&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_high_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@scores&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="n"&gt;score&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="no"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;game: &lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:score&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tap&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="vi"&gt;@scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;game: &lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;score: &lt;/span&gt;&lt;span class="n"&gt;raw_score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;raw_score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;user_high_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;
      &lt;span class="vi"&gt;@scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@scores&lt;/code&gt; hash is completely unchecked. It will grow to hold every single high score for every user — not ideal if you have a lot of either.&lt;/p&gt;

&lt;p&gt;In a Rails app, we would probably want to use &lt;code&gt;Rails.cache&lt;/code&gt; with a sensible expiry (a memory leak in Redis is still a memory leak!) instead.&lt;/p&gt;

&lt;p&gt;In a non-Rails app, we want to limit the hash size, evicting the oldest or least recently used items. &lt;a href="https://github.com/SamSaffron/lru_redux" rel="noopener noreferrer"&gt;&lt;code&gt;LruRedux&lt;/code&gt;&lt;/a&gt; is a nice implementation.&lt;/p&gt;

&lt;p&gt;A more subtle version of this leak is a cache with a limit, but whose keys are of arbitrary size. If the keys themselves grow, so too will the cache. Usually, you won't hit this. But, if you're serializing objects as JSON and using that as a key, double-check that you're not serializing things that grow with usage as well — such as a list of a user's read messages.&lt;/p&gt;

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

&lt;p&gt;Circular references &lt;em&gt;can&lt;/em&gt; be garbage collected. Garbage Collection in Ruby uses the "Mark and Sweep" algorithm. &lt;a href="https://rubykaigi.org/2021-takeout/presentations/peterzhu2118.html" rel="noopener noreferrer"&gt;During their presentation introducing variable width allocation&lt;/a&gt;, Peter Zhu and Matt Valentine-House gave an excellent explanation of how this algorithm works.&lt;/p&gt;

&lt;p&gt;Essentially, there are two phases: marking and sweeping.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the &lt;strong&gt;marking&lt;/strong&gt; phase, the garbage collector starts at root objects (classes, globals, etc.), marks them, and then looks at their referenced objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It then marks all of the referenced objects. Referenced objects that are already marked are not looked at again. This continues until there are no more objects to look at — i.e., all referenced objects have been marked.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The garbage collector then moves on to the &lt;strong&gt;sweeping&lt;/strong&gt; phase. Any object not marked is cleaned up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, objects with live references can still be cleaned up. As long as a root object does not eventually reference an object, it will be collected. In this way, clusters of objects with circular references can still be garbage collected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application Performance Monitoring: The Event Timeline and Allocated Objects Graph
&lt;/h2&gt;

&lt;p&gt;As mentioned in the &lt;a href="https://blog.appsignal.com/2022/07/27/how-to-track-down-memory-leaks-in-ruby.html" rel="noopener noreferrer"&gt;first part of this series&lt;/a&gt;, any production-level app should use some form of Application Performance Monitoring (APM).&lt;/p&gt;

&lt;p&gt;Many options are available, including rolling your own (only recommended for larger teams). One key feature you should get from an APM is the ability to see the number of allocations an action (or background job) makes. Good APM tools will break this down, giving insight into where allocations come from — the controller, the view, etc.&lt;/p&gt;

&lt;p&gt;This is often called something like an 'event timeline.' Bonus points if your APM allows you to &lt;a href="https://docs.appsignal.com/ruby/instrumentation/instrumentation.html" rel="noopener noreferrer"&gt;write custom code that further breaks down the timeline&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Consider the following code for a Rails controller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeaksController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:leak&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@leaks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vg"&gt;$leak&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;


  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;leak&lt;/span&gt;
    &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vg"&gt;$leak&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Leak&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="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When reported by an APM, the 'event timeline' might look something like the following screenshot from AppSignal.&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%2F980za53xfjihhyckb0wv.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%2F980za53xfjihhyckb0wv.png" alt="Bare Event Timeline" width="800" height="167"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This can be instrumented so we can see which part of the code makes the allocations in the timeline. In real apps, it is probably going to be less obvious from the code 😅&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeaksController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:leak&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="no"&gt;Appsignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'leak.fetch_leaks'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="vi"&gt;@leaks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vg"&gt;$leak&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;


  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;leak&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:leak&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="no"&gt;Appsignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;instrument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'leak.create_leaks'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="vg"&gt;$leak&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Leak&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="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Here's an example of an instrumented event timeline, again from AppSignal:&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%2Fws8zrpx6g2dh6ke7gk5s.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%2Fws8zrpx6g2dh6ke7gk5s.png" alt="Instrumented Event Timeline" width="800" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Knowing where to instrument can often be difficult to grasp. There's no substitute for really understanding your application's code, but there are some signals that can serve as 'smells'.&lt;/p&gt;

&lt;p&gt;If your APM surfaces GC runs or allocations over time, you can look for spikes to see if they match up with certain endpoints being hit or certain running background jobs. Here's another example from &lt;a href="https://blog.appsignal.com/2022/07/28/appsignal-for-ruby-gem-3-1-mri-vm-magic-dashboard.html" rel="noopener noreferrer"&gt;AppSignal's Ruby VM magic dashboard&lt;/a&gt;:&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%2Fhkfkwbd3r8ud03h6jevn.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%2Fhkfkwbd3r8ud03h6jevn.png" alt="Allocations" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By looking at allocations in this way, we can narrow down our search when&lt;br&gt;
looking into memory problems. This makes it much easier to use tools like&lt;br&gt;
&lt;code&gt;memory_profiler&lt;/code&gt; and &lt;code&gt;derailed_benchmarks&lt;/code&gt; efficiently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.appsignal.com/2022/07/28/appsignal-for-ruby-gem-3-1-mri-vm-magic-dashboard.html" rel="noopener noreferrer"&gt;Read about the latest additions to AppSignal's Ruby gem, like allocation and GC stats tracking&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;In this post, we dived into some tools that can help find and fix memory leaks, including &lt;code&gt;memory_profiler&lt;/code&gt;, &lt;code&gt;derailed_benchmarks&lt;/code&gt;, &lt;code&gt;perf:mem_over_time&lt;/code&gt;, &lt;code&gt;perf:objects&lt;/code&gt;, &lt;code&gt;perf:heap_diff&lt;/code&gt;, the event timeline and allocated objects graph in AppSignal.&lt;/p&gt;

&lt;p&gt;I hope you've found this post, alongside &lt;a href="https://blog.appsignal.com/2022/07/27/how-to-track-down-memory-leaks-in-ruby.html" rel="noopener noreferrer"&gt;part one&lt;/a&gt;, useful in diagnosing and sorting out memory leaks in your Ruby app.&lt;/p&gt;

&lt;p&gt;Read more about the tools we used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/SamSaffron/memory_profiler" rel="noopener noreferrer"&gt;&lt;code&gt;memory_profiler&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/zombocom/derailed_benchmarks" rel="noopener noreferrer"&gt;&lt;code&gt;derailed_benchmarks&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tony-rowan/leaky-rails-app" rel="noopener noreferrer"&gt;The leaky Rails app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additional detailed reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-3.1.2/GC.html" rel="noopener noreferrer"&gt;&lt;code&gt;GC&lt;/code&gt; module documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-3.1.2/ObjectSpace.html" rel="noopener noreferrer"&gt;&lt;code&gt;ObjectSpace&lt;/code&gt; module documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jemma.dev/blog/ruby-garbage-collection-deep-dive/" rel="noopener noreferrer"&gt;Garbage Collection Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rubykaigi.org/2021-takeout/presentations/peterzhu2118.html" rel="noopener noreferrer"&gt;Variable Width Allocation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic" rel="noopener noreferrer"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
    </item>
    <item>
      <title>How to Track Down Memory Leaks in Ruby</title>
      <dc:creator>Tony Rowan</dc:creator>
      <pubDate>Wed, 10 Aug 2022 12:51:42 +0000</pubDate>
      <link>https://dev.to/appsignal/how-to-track-down-memory-leaks-in-ruby-458g</link>
      <guid>https://dev.to/appsignal/how-to-track-down-memory-leaks-in-ruby-458g</guid>
      <description>&lt;p&gt;A memory leak is an unintentional, uncontrolled, and unending increase in memory usage. No matter how small, eventually, a leak will cause your process to run out of memory and crash. Even if you periodically restart your app to avoid this crash (no judgment, I've done that!), you still suffer the performance implications of a memory leak.&lt;/p&gt;

&lt;p&gt;In this post, the first of a two-part series on memory leaks, we'll start by looking at how Ruby manages memory, how Garbage Collection (GC) works, and how to find a leak.&lt;/p&gt;

&lt;p&gt;In the second part, we'll take a deeper dive into tracking down leaks.&lt;/p&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Ruby Memory Management
&lt;/h2&gt;

&lt;p&gt;Ruby objects are stored on the heap, and each object fills one slot on the heap.&lt;/p&gt;

&lt;p&gt;Prior to Ruby 3.1, all slots on the heap were the same size — 40 bytes, to be exact. Objects too large to fit in a slot were stored outside the heap. Each slot included a reference to where objects were moved.&lt;/p&gt;

&lt;p&gt;In Ruby 3.1, &lt;a href="https://bugs.ruby-lang.org/issues/17816" rel="noopener noreferrer"&gt;variable width allocation&lt;/a&gt; for &lt;code&gt;String&lt;/code&gt; objects was merged. Soon, variable width allocation will be the norm for all object types.&lt;/p&gt;

&lt;p&gt;Variable width allocation aims to improve performance by improving cache locality — all the information of an object will be stored in one place rather than across two memory locations.&lt;/p&gt;

&lt;p&gt;It should also simplify (some parts) of memory management. At the moment, there are two 'heaps':&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Ruby heap (or GC heap) that stores smaller Ruby objects.&lt;/li&gt;
&lt;li&gt;The C heap (or malloc/transient heap) that stores larger objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once variable width allocation is the norm, there should be no need for the latter heap.&lt;/p&gt;

&lt;p&gt;The heap starts at a given size (10,000 slots by default) and objects are assigned to free slots as they are created. When Ruby tries to create an object and there are no free slots available, Garbage Collection (GC) occurs to make some free slots available.&lt;/p&gt;

&lt;p&gt;If there are too few free slots after GC, the heap will be expanded (more on this a little later).&lt;/p&gt;

&lt;p&gt;Here are the factors you can control, alongside their &lt;a href="https://docs.ruby-lang.org/ja/latest/class/GC.html" rel="noopener noreferrer"&gt;environment variables&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial size of the heap - &lt;code&gt;RUBY_GC_HEAP_INIT_SLOTS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Number of free slots that should be available after GC occurs - &lt;code&gt;RUBY_GC_HEAP_FREE_SLOTS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Amount the heap is expanded by -&lt;code&gt;RUBY_GC_HEAP_GROWTH_FACTOR&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Garbage Collection in Ruby
&lt;/h2&gt;

&lt;p&gt;Garbage Collection in Ruby 'stops the world' — no other process occurs when GC occurs. Garbage Collection in Ruby&lt;br&gt;
(since 2.1) is also &lt;em&gt;generational&lt;/em&gt;, meaning that the garbage collector has two modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minor GC&lt;/strong&gt; - inspects 'young' objects (objects created recently)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Major GC&lt;/strong&gt; - inspects 'old' objects as well as 'young' objects (&lt;em&gt;all&lt;/em&gt; the objects)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: An 'old' object has survived &lt;code&gt;3&lt;/code&gt; GC runs, major or minor.&lt;/p&gt;

&lt;p&gt;When the heap is full, minor GC is invoked first. If it can't free up enough slots to be below the limit, major GC will be invoked. Only then, if there are still not enough free slots, will the heap be expanded.&lt;/p&gt;

&lt;p&gt;Major GC is more expensive than minor GC because it looks at more objects.&lt;/p&gt;

&lt;p&gt;The theory behind why generational GC is more performant is that objects usually fall into two categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Objects that are allocated and then quickly go out of scope.&lt;/strong&gt; In a Rails app, models fetched from the DB to render a page will go out of scope when the request ends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Objects that are allocated and kept around for a long time.&lt;/strong&gt; Classes and caches are likely to still be in use throughout the lifetime of an app.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Major GC will also run after minor GC if the number of old objects is above a certain threshold, even if there are sufficient free slots. This limit increases as the size of the heap grows and can be controlled by the &lt;code&gt;RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR&lt;/code&gt; environment variable.&lt;/p&gt;

&lt;p&gt;When you have a leak, you create objects that can't be cleaned up — more and more &lt;code&gt;old&lt;/code&gt; objects. This means that major (expensive) GC will run much more often than it should. Since nothing else runs when GC is running, this is time that you waste.&lt;/p&gt;

&lt;p&gt;I've left some links at the end of this article for further reading on memory layout and the garbage collector in Ruby.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Does A Memory Leak Look Like in Ruby?
&lt;/h2&gt;

&lt;p&gt;You can see a memory leak using simple tools available on any&lt;br&gt;
Unix system. Take the following code as an example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# leaky.rb&lt;/span&gt;

&lt;span class="n"&gt;an_array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;an_array&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"B"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"C"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;an_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;
  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To say this code 'leaks' is a little unfair — all it does is leak! — but it serves our purposes.&lt;/p&gt;

&lt;p&gt;We can observe the leak quite simply from the command line by running this program in one terminal and &lt;code&gt;watch&lt;/code&gt;-ing the memory increase over time with &lt;code&gt;ps&lt;/code&gt;.&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="c"&gt;# In terminal one&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;ruby ./leaky.rb

&lt;span class="c"&gt;# In terminal two&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;watch ps &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"ruby ./leaky.rb"&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; pmem,pcpu,rss,args
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pgrep -f "ruby ./leaky.rb"&lt;/code&gt; finds the process ID for us, so that we can restrict the &lt;code&gt;ps&lt;/code&gt; output to only the process we're interested in. As you may be able to guess, it's like &lt;code&gt;grep&lt;/code&gt; for processes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;watch&lt;/code&gt; tool allows us to poll the output of a given command and update it in place, giving us a live dashboard within our terminal.&lt;/p&gt;

&lt;p&gt;You'll get output like this, which updates every couple of seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Every 2.0s: ps &lt;span class="nt"&gt;-p&lt;/span&gt; 50866 &lt;span class="nt"&gt;-o&lt;/span&gt; pmem,pcpu,rss,args

%MEM  %CPU    RSS ARGS
 0.2   4.1 163408 /Users/tonyrowan/.asdf/installs/ruby/3.1.1/bin/ruby ./leaky.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the &lt;code&gt;%MEM&lt;/code&gt; and &lt;code&gt;RSS&lt;/code&gt; increasing. They are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;%MEM&lt;/code&gt;&lt;/strong&gt; - The amount of memory the process uses as a percentage of memory on the host machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RSS&lt;/code&gt;&lt;/strong&gt; (resident set size) - The amount of RAM the process uses in bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This basic OS-only information is enough to spot if you have a leak — if the memory keeps going up, it means you do!&lt;/p&gt;

&lt;h2&gt;
  
  
  Find Ruby Leaks with the Garbage Collector Module
&lt;/h2&gt;

&lt;p&gt;We can also detect leaks within Ruby code itself with the &lt;code&gt;GC&lt;/code&gt; module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# leaky.rb&lt;/span&gt;

&lt;span class="no"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disable&lt;/span&gt; &lt;span class="c1"&gt;# Only run GC when manually called&lt;/span&gt;

&lt;span class="n"&gt;an_array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;an_array&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"B"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"C"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Array is &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;an_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; items long"&lt;/span&gt;

  &lt;span class="no"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt; &lt;span class="c1"&gt;# Run a major GC - use full_mark: false for minor GC&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"There are &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:heap_live_slots&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; live objects"&lt;/span&gt;

  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;GC.stat&lt;/code&gt; method will return a hash with a lot of useful information. Here, we're interested in &lt;code&gt;:heap_live_slots&lt;/code&gt;, which is the number of slots on the heap that are in use. That's the opposite of &lt;code&gt;:heap_free_slots&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At the end of the loop, we force a major GC and print out the number of used slots, i.e., the number of objects that remain after GC.&lt;/p&gt;

&lt;p&gt;When we run our little program, we see this increase ad infinitum. We have a leak! We could also have used &lt;code&gt;GC.stat(:old_objects)&lt;/code&gt; to the same effect.&lt;/p&gt;

&lt;p&gt;While the &lt;code&gt;GC&lt;/code&gt; module can be used to see &lt;em&gt;if&lt;/em&gt; we have a leak and (if you're smart with your &lt;code&gt;puts&lt;/code&gt; statements) where the leak might be occurring, we can see the type of objects that might be leaking with the &lt;code&gt;ObjectSpace&lt;/code&gt; module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# leaky.rb&lt;/span&gt;

&lt;span class="no"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disable&lt;/span&gt; &lt;span class="c1"&gt;# Only run GC when manually called&lt;/span&gt;

&lt;span class="n"&gt;an_array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;an_array&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"A"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"B"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;"C"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Array is &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;an_array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; items long"&lt;/span&gt;

  &lt;span class="no"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt; &lt;span class="c1"&gt;# Run a major GC - use full_mark: false for minor GC&lt;/span&gt;
  &lt;span class="n"&gt;pp&lt;/span&gt; &lt;span class="no"&gt;ObjectSpace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count_objects&lt;/span&gt;

  &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ObjectSpace.count_objects&lt;/code&gt; method returns a hash with the counts of live objects. &lt;code&gt;T_STRING&lt;/code&gt;, for instance, is the number of strings live in memory. For our rather leaky program, this value increases with each loop, even after GC. We can see that we are leaking string objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application Performance Monitoring in Production with AppSignal
&lt;/h2&gt;

&lt;p&gt;While playing with &lt;code&gt;ps&lt;/code&gt; and &lt;code&gt;GC&lt;/code&gt; can be a sensible route for toy projects — they're also fun and informative to use! — I would &lt;em&gt;not&lt;/em&gt; recommend them as your memory leak detection solution in production apps.&lt;/p&gt;

&lt;p&gt;This is where you would use an Application Performance Monitoring (APM) tool. If you're a very large company, you can build these yourself. For smaller outfits, though, picking an APM off-the-shelf is the way to go. You do need to pay a monthly subscription, but the information they provide more than makes up for it.&lt;/p&gt;

&lt;p&gt;For detecting memory leaks, you want to find server or process memory use (sometimes called RSS) graphs over time. Here's an example screenshot from AppSignal's 'process memory usage' dashboard of a healthy app shortly after being deployed:&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%2Ffh4d0unpwhjwgkeqtqnu.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%2Ffh4d0unpwhjwgkeqtqnu.png" alt="Healthy App RSS" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here's an unhealthy app after deployment:&lt;br&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%2Fmhaz37tnl54qf9uaosnz.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%2Fmhaz37tnl54qf9uaosnz.png" alt="Unhealthy App RSS" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AppSignal will even surface Ruby VM stats like GC and heap slots, which can&lt;br&gt;
give you an even clearer signal for a memory leak. If the number of live slots&lt;br&gt;
keeps growing, you have a leak!&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%2F3um8hvtyfwtebwoxxa6k.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%2F3um8hvtyfwtebwoxxa6k.png" alt="Heap Slots showing leak" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.appsignal.com/ruby" rel="noopener noreferrer"&gt;Read more about AppSignal for Ruby&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap Up and Further Reading
&lt;/h2&gt;

&lt;p&gt;In this post, we took a quick tour of Ruby's memory management and garbage collector. We then diagnosed how to discover a memory leak using Unix tools and Ruby's GC module.&lt;/p&gt;

&lt;p&gt;Next time, we'll see how to use &lt;code&gt;memory_profiler&lt;/code&gt; and &lt;code&gt;derailed_benchmarks&lt;/code&gt; to find and fix leaks.&lt;/p&gt;

&lt;p&gt;In the meantime, you can read more about the tools we used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://linux.die.net/man/1/watch" rel="noopener noreferrer"&gt;&lt;code&gt;watch&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linux.die.net/man/1/ps" rel="noopener noreferrer"&gt;&lt;code&gt;ps&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://linux.die.net/man/1/pgrep" rel="noopener noreferrer"&gt;&lt;code&gt;pgrep&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additional further reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-3.1.2/GC.html" rel="noopener noreferrer"&gt;&lt;code&gt;GC&lt;/code&gt; module documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ruby-doc.org/core-3.1.2/ObjectSpace.html" rel="noopener noreferrer"&gt;&lt;code&gt;ObjectSpace&lt;/code&gt; module documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jemma.dev/blog/ruby-garbage-collection-deep-dive/" rel="noopener noreferrer"&gt;Garbage Collection Deep Dive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://rubykaigi.org/2021-takeout/presentations/peterzhu2118.html" rel="noopener noreferrer"&gt;Variable Width Allocation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy coding, and see you next time!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, &lt;a href="https://blog.appsignal.com/ruby-magic" rel="noopener noreferrer"&gt;subscribe to our Ruby Magic newsletter and never miss a single post&lt;/a&gt;!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
    </item>
    <item>
      <title>Adding a Star Rating with Hotwire</title>
      <dc:creator>Tony Rowan</dc:creator>
      <pubDate>Thu, 14 Jul 2022 08:46:21 +0000</pubDate>
      <link>https://dev.to/tonyrowan/adding-a-star-rating-with-hotwire-4p02</link>
      <guid>https://dev.to/tonyrowan/adding-a-star-rating-with-hotwire-4p02</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/es/@grandbeau?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;GRANDBEAU&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/hot-wire?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post walks through the process of adding a rating system to a &lt;a href="https://turbo.hotwire.dev" rel="noopener noreferrer"&gt;Turbo&lt;/a&gt; powered single page application. The application in question was built up in a &lt;a href="https://tonyrowan.tech/2021/12/04/making-a-single-page-search-with-turbo" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;. If you want to follow along with the post, you can grab the repo at the &lt;a href="https://github.com/tony-rowan/turbo-movies/tree/codealong--star-bar--start" rel="noopener noreferrer"&gt;starting point&lt;/a&gt; or just read the &lt;a href="https://github.com/tony-rowan/turbo-movies/pull/1" rel="noopener noreferrer"&gt;pull request&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;In this walkthrough, we're going to be adding a basic star bar rating system to our movies single page application. You might be surprised to see that we can get a basic working version of this with no additional javascript whatsoever!&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%2Fuser-images.githubusercontent.com%2F2379659%2F148576104-cdef7b24-750f-4b81-b8db-200effcbae4b.gif" 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%2Fuser-images.githubusercontent.com%2F2379659%2F148576104-cdef7b24-750f-4b81-b8db-200effcbae4b.gif" alt="Demo of the Star Bar Rating" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Ratings
&lt;/h2&gt;

&lt;p&gt;The first step is to add ratings to our movies. Run the following in your terminal to create a migration file that will add a new column to our &lt;code&gt;movies&lt;/code&gt; table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails generate migration add_rating_to_movies rating:integer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the file and edit it to add a not-null constraint and a default value of &lt;code&gt;0&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AddRatingToMovies&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;7.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:movies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also add a validation to our &lt;code&gt;Movie&lt;/code&gt; model to ensure the rating stays within a sane bound that we can display on our star bar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Movie&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;numericality: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;in: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can run &lt;code&gt;db:migrate&lt;/code&gt; to add the column. If you open&lt;br&gt;
up a console with  &lt;code&gt;bin/rails console&lt;/code&gt; now you'll be able to update the rating on a movie  e.g. &lt;code&gt;Movie.first.update(rating: 5)&lt;/code&gt;. Our validation will prevent you from setting the rating to something we can't display like &lt;code&gt;-1&lt;/code&gt; or &lt;code&gt;6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now we can update the view for our movies to display the rating.&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%2Fahxdprovizptfomapvi4.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%2Fahxdprovizptfomapvi4.png" alt="Demo of the text rating" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's pretty basic though, we can improve on that.&lt;/p&gt;
&lt;h2&gt;
  
  
  Add the Star Bar
&lt;/h2&gt;

&lt;p&gt;The next step in creating our star bar is the stars! We're going to use icons from the wonderful &lt;a href="https://heroicons.com" rel="noopener noreferrer"&gt;heroicons&lt;/a&gt; which is a suite of beautiful SVG icons from the makers of &lt;a href="https://tailwindcss.com" rel="noopener noreferrer"&gt;TailwindCSS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There's an icon called &lt;code&gt;star&lt;/code&gt; that we'll use, copy the SVG contents of the icon and paste it into a file called &lt;code&gt;app/assets/images/star.svg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The best way to &lt;a href="http://css-tricks.com/using-svg" rel="noopener noreferrer"&gt;style an SVG&lt;/a&gt; icon (which is one of the big advantages of the format, in my opinion) is to embed the SVG in the HTML document (render it inline with the rest of the HTML) and from there apply CSS styles as you might expect. There's a gem called &lt;a href="https://github.com/jamesmartin/inline_svg" rel="noopener noreferrer"&gt;&lt;code&gt;inline_svg&lt;/code&gt;&lt;/a&gt; that we'll make use of to make that easier.&lt;/p&gt;

&lt;p&gt;Add to your &lt;code&gt;Gemfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s2"&gt;"inline_svg"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run in your terminal:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Now we can use the &lt;code&gt;inline_svg_tag&lt;/code&gt; in our views.&lt;/p&gt;

&lt;p&gt;To render our star bar we need to swap out the text rating we just added and replace it with this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-x-3 mb-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MAX_RATING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;inline_svg_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"star.svg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"w-8 h-8 &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;star_rating_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@movie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We going to render stars using the &lt;code&gt;inline_svg_tag&lt;/code&gt; a total of&lt;br&gt;
&lt;code&gt;Movie::MAX_RATING&lt;/code&gt; number of times. This is a constant we've introduced for the maximum rating i.e. the maximum  number of stars. To avoid duplication, we should reuse it in the validation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Movie&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="no"&gt;MAX_RATING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

  &lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:rating&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;numericality: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;in: &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="no"&gt;MAX_RATING&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've also extracted a new helper method &lt;code&gt;star_rating_class&lt;/code&gt; so we can keep the template readable by extracting the logic for which CSS classes the star should have, based on what rating the movie has. If the movie has a rating of &lt;code&gt;4&lt;/code&gt; then&lt;br&gt;
stars with index &lt;code&gt;0&lt;/code&gt; through &lt;code&gt;3&lt;/code&gt; should be solid yellow and the last star, with index &lt;code&gt;4&lt;/code&gt;, should be a grey outline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/helpers/rating_helper.rb&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;RatingHelper&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;star_rating_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rating&lt;/span&gt;
      &lt;span class="s2"&gt;"fill-yellow-400 stroke-yellow-400"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="s2"&gt;"fill-transparent stroke-gray-400"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fai0r56o8q1d7ksnv51o4.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%2Fai0r56o8q1d7ksnv51o4.png" alt="Demo of static star bar" width="800" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our star bar looks a lot nicer, but it's still not really doing anything. Let's change that next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Submit a Rating
&lt;/h2&gt;

&lt;p&gt;We want users to be able to click a star and apply that as a rating. We will need to add the &lt;code&gt;update&lt;/code&gt; action to the routes for &lt;code&gt;movies&lt;/code&gt; and implement that action in the &lt;code&gt;MoviesController&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;

&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:movies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%i[index show update]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# app/contollers/movies_controller.rb&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
  &lt;span class="vi"&gt;@movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="vi"&gt;@movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;rating: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:movie&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="ss"&gt;:rating&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:show&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;update&lt;/code&gt; action is fairly simple CRUD, but we only support updating the &lt;code&gt;rating&lt;/code&gt; so there's no need for mass assignment. We're also rendering the &lt;code&gt;show&lt;/code&gt; template at the end where we would normally redirect back to the &lt;code&gt;show&lt;/code&gt; action. Redirecting would work here, but we're going to make use of not redirecting later when we add feedback.&lt;/p&gt;

&lt;p&gt;In the template, we just need to turn the stars into buttons that will submit a form to the update endpoint &lt;code&gt;PATCH movies/:id&lt;/code&gt;. We'll use the &lt;a href="https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to" rel="noopener noreferrer"&gt;&lt;code&gt;button_to&lt;/code&gt;&lt;/a&gt; template helper to do this. We'll set the &lt;code&gt;method&lt;/code&gt; to &lt;code&gt;:patch&lt;/code&gt;, the default is &lt;code&gt;:post&lt;/code&gt;. We also need to add the index of the star&lt;br&gt;
(plus &lt;code&gt;1&lt;/code&gt; to account for the &lt;code&gt;0&lt;/code&gt;-based index) as a parameter in the form. In the HTML, this will be a hidden field with the name &lt;code&gt;movie[rating]&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-x-3 mb-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MAX_RATING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="n"&gt;movie_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@movie&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;method: :patch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;movie: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;rating: &lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;inline_svg_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"star.svg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"
          w-8 h-8
          hover:fill-yellow-500 hover:stroke-yellow-500
          &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;star_rating_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@movie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
        "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you try clicking stars now, you'll notice that it works perfectly, without any page refreshes. How? Turbo is taking over the form submission so that it is asynchronous, and when the response comes back and includes the &lt;code&gt;:details&lt;/code&gt; turbo-frame, it swaps out the contents for the contents of the frame in the response.&lt;/p&gt;

&lt;p&gt;In previous versions of Turbo, you could not return HTML from a form submission unless the status code was an error, successful submissions had to redirect. Fortunately for us, this is no longer a restriction.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Feedback
&lt;/h2&gt;

&lt;p&gt;The star bar is now working as intended, but it happens so seamlessly, it's not obvious that the user's rating has been persisted. We can improve this with a little user feedback. We can add a small 'toast' next to the star bar after the user rates a movie.&lt;/p&gt;

&lt;p&gt;Let's start by adding and styling our toast. It's just some plain HTML next to the star bar, coupled with a CSS animation to make it disappear after a short time. The icons used are also from heroicons. I've used the &lt;code&gt;check-circle&lt;/code&gt; and &lt;code&gt;exclamation&lt;/code&gt; icons.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.toast&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;toast&lt;/span&gt; &lt;span class="m"&gt;3s&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;toast&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;


  &lt;span class="err"&gt;3&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;


  &lt;span class="err"&gt;80&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;visible&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;


  &lt;span class="err"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&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;By setting &lt;code&gt;visibility: hidden&lt;/code&gt; we remove the toast from the accessibility tree and focus events, though it still affects layout. We use this over &lt;code&gt;display: none&lt;/code&gt; since it can be used in an animation, we would need to use javascript to apply a change to the &lt;code&gt;display&lt;/code&gt; attribute and it doesn't matter to us here that it still affects layout - you might argue that it's&lt;br&gt;
better since nothing will move when the toast completes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/movies/show.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Within the same flex group as the stars
     but after all the stars have been rendered --&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@toast&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-x-1 px-4 py-2
    bg-black/75 rounded text-white text-sm
    relative top-[-2px]
    toast"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@toast&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:success&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;inline_svg_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"check.svg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"h-4 w-4 text-green-500 relative top-px"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Rating Added!&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;inline_svg_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"warning.svg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"h-4 w-4 text-red-500 relative top-px"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Rating could not be added&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@toast&lt;/code&gt; variable comes from the &lt;code&gt;update&lt;/code&gt; action. This is why we chose to render the &lt;code&gt;:show&lt;/code&gt; template, rather than redirecting. Though we could achieve the same effect with a redirect by making use of the &lt;code&gt;flash&lt;/code&gt; object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
  &lt;span class="vi"&gt;@movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;rating: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:movie&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="ss"&gt;:rating&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="vi"&gt;@toast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:success&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="vi"&gt;@toast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:warning&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:show&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We simply set the &lt;code&gt;@toast&lt;/code&gt; variable to whether or not the update happened successfully. Since the &lt;code&gt;show&lt;/code&gt; action does not define the &lt;code&gt;@toast&lt;/code&gt; variable, the message only shows in response to a call to the &lt;code&gt;update&lt;/code&gt; action, i.e. clicking on a star.&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%2Fuser-images.githubusercontent.com%2F2379659%2F148576117-2ffafcce-3e74-47d7-af57-f1232b172747.gif" 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%2Fuser-images.githubusercontent.com%2F2379659%2F148576117-2ffafcce-3e74-47d7-af57-f1232b172747.gif" alt="A Demo of the Toast" width="760" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Filling the Star Bar
&lt;/h2&gt;

&lt;p&gt;If you've ever used a star bar in the past (I'm guessing you have) you'll have noticed that they tend to fill up from 0 to the point you hover over. This is an effect we can reproduce with a small amount of stimulus.&lt;/p&gt;

&lt;p&gt;To start with, create a new stimulus controller by running this in your terminal. This will make sure your stimulus manifest is up to date (not applicable if you're using webpacker), as well as add the boilerplate for the controller and create a correctly named file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails generate stimulus starBar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The basic expectation of the star bar is that when you hover over a star, that star and all the stars within an index lower than that star will show their hover state. When not hovering over any star, the star bar should simply display the rating.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;StarBarController&lt;/code&gt; looks like this.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;star&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hover&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nf"&gt;enter&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_fillToStar&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;starIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;leave&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_fillToStar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;_fillToStar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;star&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;starTargets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&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="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;star&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hoverClass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hoverClass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;The two actions &lt;code&gt;enter&lt;/code&gt; and &lt;code&gt;leave&lt;/code&gt; simply toggle the hover class on the appropriate stars, depending on which star the user is hovering over or none if the user is not hovering over any stars.&lt;/p&gt;

&lt;p&gt;The controller has one target called &lt;code&gt;star&lt;/code&gt;. Since we expect a list of these (the 5 star icons) we're using &lt;code&gt;this.starTargets&lt;/code&gt; in the plural to access them. The controller also has a &lt;code&gt;class&lt;/code&gt; called &lt;code&gt;hover&lt;/code&gt; - we use this so we can define the hover class for the stars in the markup rather than hardcode them in the controller.&lt;/p&gt;

&lt;p&gt;Now we just need to add to markup to connect our star bar to the controller. &lt;/p&gt;

&lt;p&gt;Our star bar now looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-x-3 mb-4"&lt;/span&gt;
  &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"star-bar"&lt;/span&gt;
  &lt;span class="na"&gt;data-star-bar-hover-class=&lt;/span&gt;&lt;span class="s"&gt;"fill-yellow-500 stroke-yellow-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MAX_RATING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="n"&gt;movie_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@movie&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="ss"&gt;method: :patch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;movie: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;rating: &lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;inline_svg_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"star.svg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"w-8 h-8 &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;star_rating_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@movie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;star_bar_target: &lt;/span&gt;&lt;span class="s2"&gt;"star"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;star_bar_star_index_param: &lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"pointerenter-&amp;gt;star-bar#enter
            pointerleave-&amp;gt;star-bar#leave"&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- the toast is omitted, but would be here --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The definition for the &lt;code&gt;hover&lt;/code&gt; classes, &lt;code&gt;data-star-bar-hover-class&lt;/code&gt; has to be on the same HTML element that attaches the controller. We also mark each of the stars as part of the &lt;code&gt;star&lt;/code&gt; target with &lt;code&gt;data-star-bar-target="star"&lt;/code&gt;, we don't have to treat it differently just because it is going to be used as an array within the controller.&lt;/p&gt;

&lt;p&gt;When the user hovers over a star we need to toggle the hover class on for each of the stars up to the star the user hovered over. We can pass parameters to our stimulus controller actions. In our case, we need to pass the index of the&lt;br&gt;
star the user hovered over. This is what the &lt;code&gt;data-star-bar-star-index-param="n"&lt;/code&gt; is doing. We can then access the parameter in the action with &lt;code&gt;event.params.starIndex&lt;/code&gt;. The generic pattern for this is &lt;code&gt;data-controller-name-param-name="value"&lt;/code&gt;, the parameter can then be accessed from the action with &lt;code&gt;event.params.paramName&lt;/code&gt; - the parameters name is&lt;br&gt;
converted to snakeCase, just like the controller names are.&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%2Fuser-images.githubusercontent.com%2F2379659%2F148576104-cdef7b24-750f-4b81-b8db-200effcbae4b.gif" 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%2Fuser-images.githubusercontent.com%2F2379659%2F148576104-cdef7b24-750f-4b81-b8db-200effcbae4b.gif" alt="Demo of the Star Bar Rating" width="720" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This star bar is very limited, I only really did it for some fun. For a more complete implementation of a star rating bar, check out a component library like &lt;a href="https://shoelace.style/components/rating" rel="noopener noreferrer"&gt;Shoelace&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We've managed to create a fully-featured, interactive star rating bar and most of the changes were in our HTML. We didn't &lt;em&gt;have&lt;/em&gt; to write any javascript if we didn't want to.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>hotwire</category>
    </item>
    <item>
      <title>Making a Single Page Search with Hotwire</title>
      <dc:creator>Tony Rowan</dc:creator>
      <pubDate>Wed, 13 Jul 2022 08:57:30 +0000</pubDate>
      <link>https://dev.to/tonyrowan/making-a-single-page-search-with-hotwire-4dhb</link>
      <guid>https://dev.to/tonyrowan/making-a-single-page-search-with-hotwire-4dhb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@timmeyer?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Tim Meyer&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/turbo?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://turbo.hotwire.dev" rel="noopener noreferrer"&gt;Turbo&lt;/a&gt; (part of the &lt;a href="https://hotwire.dev" rel="noopener noreferrer"&gt;Hotwire&lt;/a&gt; suite) is a supercharged version of Turbolinks. If you're used to immediately turning off Turbolinks in any new project, you might be surprised to learn that you don't need React or Vue to build a rich and interactive web app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;We're going to be adding a single page search to a simple Single Page App (SPA) that allows users to view information about some Movies.&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%2Ffmdto5sq70828h5sy2fr.gif" 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%2Ffmdto5sq70828h5sy2fr.gif" alt="Demo of the end result" width="760" height="579"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Starting Point
&lt;/h2&gt;

&lt;p&gt;The starting point is heavily based on '&lt;a href="https://www.mikewilson.dev/posts/using-hotwire-with-rails-for-a-spa-like-experience" rel="noopener noreferrer"&gt;Using Hotwire with Rails for a SPA like experience&lt;/a&gt;' by Mike Wilson. If you want to follow along with this post you can use the starting point &lt;a href="https://github.com/tony-rowan/turbo-movies/tree/single-page-search-start" rel="noopener noreferrer"&gt;here on Github&lt;/a&gt; or follow along through that post. You should have a simple app where you can select a movie without reloading the page, with only a very simple rails controller.&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%2Fxcgq4cwltoo3up1359hh.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%2Fxcgq4cwltoo3up1359hh.png" alt="Demo of starting point" width="800" height="586"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MoviesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@movies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="vi"&gt;@movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding Search
&lt;/h2&gt;

&lt;p&gt;The first step is to add search support to the controller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MoviesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="vi"&gt;@movies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;movies&lt;/span&gt;
    &lt;span class="vi"&gt;@query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="vi"&gt;@movie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;movies&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
      &lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"title ILIKE ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&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;else&lt;/span&gt;
      &lt;span class="no"&gt;Movie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can verify this is working by simply adding the &lt;code&gt;query&lt;/code&gt; parameter to the URL and visiting the page again. Only the movies matching the query you made are present in the list.&lt;/p&gt;

&lt;p&gt;Not very user friendly though. Let's add a search form to the top of the list of movies, within the &lt;code&gt;turbo-frame-tag&lt;/code&gt; named &lt;code&gt;:index&lt;/code&gt; - the same turbo-frame with the list of movies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;movies_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;method: :get&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="vi"&gt;@query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"Search"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you make a search and hit enter, the list of movies is filtered. You'll also notice that, if you have a movie selected, it stays selected. Searching doesn't cause a full page refresh, only the list of movies updates.&lt;/p&gt;

&lt;p&gt;This happens because each turbo-frame creates its own navigational context. By default, all links and form submissions from within a given turbo-frame only effect that turbo-frame. Under the hood, turbo is hijacking the submission event, making an AJAX call for the form submission and interpreting the result. &lt;/p&gt;

&lt;p&gt;When the submission is successful turbo reads the response, looking for a &lt;code&gt;turbo-frame-tag&lt;/code&gt; that has the same name as the turbo frame that caused the submission. If it finds a match, it swaps out the contents.&lt;/p&gt;

&lt;p&gt;In our case, the form submission returns the same page so it is pretty clear that there will be a matching turbo-frame - the &lt;code&gt;:index&lt;/code&gt; frame.&lt;/p&gt;

&lt;p&gt;I said this was the default behaviour. By adding a &lt;code&gt;turbo-target&lt;/code&gt; attribute to a form or link turbo will instead look for, and swap the contents in, the turbo frame with the name you passed - this is how clicking the movies changes the &lt;code&gt;:details&lt;/code&gt; turbo frame and not the &lt;code&gt;:index&lt;/code&gt; frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  Come Alive with Stimulus
&lt;/h2&gt;

&lt;p&gt;This is pretty good, but what if the user &lt;em&gt;didn't&lt;/em&gt; have to press enter when they were done searching? We can search as the user types and we can do this with very little Javascript thanks to &lt;a href="https://stimulus.hotwire.dev" rel="noopener noreferrer"&gt;Stimulus&lt;/a&gt; - another part of the Hotwire suite.&lt;/p&gt;

&lt;p&gt;We can watch for input changes on the text field of the search form and call an action on a Stimulus controller to submit the form for the user. This can be achieved with a small amount of markup on the form and the text field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="n"&gt;movies_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;method: :get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;controller: &lt;/span&gt;&lt;span class="s2"&gt;"submit-form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;submit_form_target: &lt;/span&gt;&lt;span class="s2"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="vi"&gt;@query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;placeholder: &lt;/span&gt;&lt;span class="s2"&gt;"Search"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;data: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;action: &lt;/span&gt;&lt;span class="s2"&gt;"input-&amp;gt;submit-form#submit"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the text field we've added &lt;code&gt;data-action="input-&amp;gt;submit-form#submit"&lt;/code&gt;. The value assigned to the &lt;code&gt;data-action&lt;/code&gt; attribute encodes which event to listen for and which controller and action (method) to invoke when the event happens. The generic markup is &lt;code&gt;event-&amp;gt;controller-name#action&lt;/code&gt;. In our example, &lt;code&gt;event&lt;/code&gt; is &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;controller&lt;/code&gt; is &lt;code&gt;submit-form&lt;/code&gt; and our &lt;code&gt;action&lt;/code&gt; is &lt;code&gt;submit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;input&lt;/code&gt; event name comes from the &lt;a href="https://www.w3schools.com/js/js_htmldom_events.asp" rel="noopener noreferrer"&gt;JS DOM events&lt;/a&gt;, and we want to listen to any changes in the input. The controller name &lt;code&gt;submit-form&lt;/code&gt; is just the name we gave the controller we want to invoke here - it is expected to be defined in a file named &lt;code&gt;submit_form_controller.js&lt;/code&gt;. Its name in Javascript land will then be &lt;code&gt;SubmitFormController&lt;/code&gt;. The &lt;code&gt;submit&lt;/code&gt; action is just a public method defined on that controller.&lt;/p&gt;

&lt;p&gt;On the form element we've added &lt;code&gt;data-controller="submit-form"&lt;/code&gt; and &lt;code&gt;data-submit-form-target="form"&lt;/code&gt;. The former simply tells Stimulus to attach a new instance of the &lt;code&gt;SubmitFormController&lt;/code&gt; to the form element, allowing us to invoke its actions from the form element or any of its children. The latter attribute makes the form element available from the controller as a 'target' called &lt;code&gt;form&lt;/code&gt; - from the controller we will be able to access the element by calling &lt;code&gt;this.formTarget&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;SubmitFormController&lt;/code&gt; is very simple.&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestSubmit&lt;/span&gt;&lt;span class="p"&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;When the &lt;code&gt;submit&lt;/code&gt; action is called it submits the target, which is assumed to be a form. The action itself is debounced. This stops us from generating too many simultaneous requests which makes the submissions overlap.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;this.formTarget.requestSubmit();&lt;/code&gt; is very important, if we used &lt;code&gt;submit()&lt;/code&gt; rather than &lt;code&gt;requestSubmit()&lt;/code&gt;, the event that Turbo hooks into the intercept the form submission would not happen and you would have a normal form submission - with a full page refresh.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Small Problem
&lt;/h3&gt;

&lt;p&gt;This works really well - until the first time the form is submitted - then the focus in the text field is lost and we can't keep typing. That's definitely a bug!&lt;/p&gt;

&lt;p&gt;What's happening here is that when the response is returned, we're replacing the whole contents of the &lt;code&gt;:index&lt;/code&gt; turbo frame - including the search form itself. To avoid this we can move the search form outside of the turbo frame, and then add &lt;code&gt;turbo-target: :index&lt;/code&gt; to the form element. This will mean when the form is submitted, turbo will replace the contents of the &lt;code&gt;:index&lt;/code&gt; turbo-frame and leave the search field completely untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And there we have it. We've added a single page search to an app and all we needed was a small amount of markup and very small amount of Javascript. That's the power of Turbo.&lt;/p&gt;

&lt;p&gt;The resulting code can be found on &lt;a href="https://github.com/tony-rowan/turbo-movies" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>hotwire</category>
    </item>
    <item>
      <title>Weekend Project: Pokémon Name Generator</title>
      <dc:creator>Tony Rowan</dc:creator>
      <pubDate>Tue, 12 Jul 2022 14:56:40 +0000</pubDate>
      <link>https://dev.to/tonyrowan/weekend-project-pokemon-name-generator-3n8d</link>
      <guid>https://dev.to/tonyrowan/weekend-project-pokemon-name-generator-3n8d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@jontyson?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Jon Tyson&lt;/a&gt; on &lt;a href="https://unsplash.com/s/photos/name-generator?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Like many millenials, I grew playing/watching/collecting Pokémon. Back then, there were only 151, but that was still enough variety to make naming all of them quite difficult - even if you were fan. For parents and the uninterested, the names were quite difficult to keep in one's head. Now there's almost 1000 and some of them are quite strange. Looking at you "klink, klang, and klinklang".&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%2Fi2.wp.com%2Fmetro.co.uk%2Fwp-content%2Fuploads%2F2016%2F06%2Fash_s_pikachu-0.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%2Fi2.wp.com%2Fmetro.co.uk%2Fwp-content%2Fuploads%2F2016%2F06%2Fash_s_pikachu-0.png" alt="Pikachu" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ah yes, pokechu! The electric mouse Pokémon!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With so many Pokémon now, I thought it would be fun to see if I could generate names that sounded like real Pokémon using ruby.&lt;/p&gt;

&lt;h2&gt;
  
  
  Naïve Attempt
&lt;/h2&gt;

&lt;p&gt;My first thought was to simply create names of roughly the right length with roughly the right letters. Simply gather these stats from the corpus of actual Pokémon names and then generate names that match these stats.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;letter_count_distribution: &lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="ss"&gt;letter_distribution: &lt;/span&gt;&lt;span class="n"&gt;names&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="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:letter_count_distribution&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:letter_distribution&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That didn't work. It produced unpronouncable nonsense.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eareredneemoo
gedgfa
lbphoobng
ureerbeier
airmr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem was that this naïve algorithm doesn't take into account letter to letter changes. The letters &lt;code&gt;lbph&lt;/code&gt; can't all appear next to each other.&lt;/p&gt;

&lt;p&gt;That sounds like what a Markov Chain is for!&lt;/p&gt;

&lt;h2&gt;
  
  
  Markov Chains
&lt;/h2&gt;

&lt;p&gt;I'm not an AI or language processing expert, but I vaguely remember covering Markov Chains at uni. Their main use is predication. For instance, predicting which word you mean, given the first few letters you've typed.&lt;/p&gt;

&lt;p&gt;I wasn't trying to predict which name the user was typing as they were typing it - I was trying to create new names. So I had to use the concepts of a Markov Chain a little loosely.&lt;/p&gt;

&lt;p&gt;At the core of Markov Chains is the endcoding of all the valid ways to get from one letter (or phoneme or word or w/e) to the next, including which letters can start the name and which names can end it. That's exactly what was missing from the naïve algorithm.&lt;/p&gt;

&lt;p&gt;So instead of simply gathering stats on length and letter frequency, I built up map of valid letter transitions and their relative frequencies. From this map all I had to do was choose a random letter to start, and then random subsequent letters until the name ended when it selected &lt;code&gt;nil&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# create a statistics map of all the valid transitions&lt;/span&gt;
&lt;span class="n"&gt;markov_statistics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="n"&gt;pokemon_names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;last_letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

  &lt;span class="nb"&gt;name&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="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;last_letter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;last_letter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;last_letter&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="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;last_letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;last_letter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;last_letter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;last_letter&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="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# create the name by picking valid letters&lt;/span&gt;
&lt;span class="n"&gt;pokemon_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="n"&gt;current_letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;

&lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_letter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

  &lt;span class="n"&gt;pokemon_name&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;current_letter&lt;/span&gt;
  &lt;span class="n"&gt;current_letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;current_letter&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;pokemon_name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;markov_statistics&lt;/code&gt; is a hash that encodes all the valid transitions in the corpus. The key of the hash is the start letter and the values of the hash are all the letters that might follow the start letter. For letters that ended the&lt;br&gt;
name &lt;code&gt;nil&lt;/code&gt; is included in the value array.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;corpus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bulbasaur"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"charmander"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"squirtle"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kp"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="s2"&gt;"a"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"r"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"u"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="s2"&gt;"b"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"b"&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;I allowed letters to be included in the value array multiple times so that when I choose a letter from the value array with &lt;code&gt;.sample&lt;/code&gt; it was weighted to the frequency of that pair in the corpus. That way more common pairs would be chosen more often.&lt;/p&gt;

&lt;p&gt;This produced better names, but some of them only had one letter in them. How is that valid?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;s
munegogunchezatoy
pitulor
kiterlving
higraponeo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Context
&lt;/h3&gt;

&lt;p&gt;The problem was that &lt;code&gt;nil-&amp;gt;s&lt;/code&gt; was valid, and so was &lt;code&gt;s-&amp;gt;nil&lt;/code&gt;. So, the algorithm could choose &lt;code&gt;nil-&amp;gt;s-&amp;gt;nil&lt;/code&gt; - it thinks &lt;code&gt;s&lt;/code&gt; is a perfectly valid name!&lt;/p&gt;

&lt;p&gt;This fix for this was to introduce some context. In markov chains, we consider all the letters typed so far. I didn't want to consider all the letters in the name before choosing the next letter - then it would only produce actual Pokémon names! But, I could consider more that one letter at a time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# create statistics of valid transitions,&lt;/span&gt;
&lt;span class="c1"&gt;# given a configurable length of context&lt;/span&gt;
&lt;span class="n"&gt;markov_statistics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="n"&gt;pokemon_names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="nb"&gt;name&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="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;context&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="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context_length&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;markov_statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;context&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="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# create the name by picking valid letters,&lt;/span&gt;
&lt;span class="c1"&gt;# keeping a configurable length of context&lt;/span&gt;
&lt;span class="n"&gt;pokemon_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;current_letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;

&lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_letter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;

  &lt;span class="n"&gt;pokemon_name&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;current_letter&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;current_letter&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;context_length&lt;/span&gt;
  &lt;span class="n"&gt;current_letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;pokemon_name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this version &lt;code&gt;markov_statistics&lt;/code&gt; contains a map where the keys are a list of letters that form some context and the values are the possible next letters e.g. &lt;code&gt;[b, u, l]-&amp;gt;[b, g, ...]&lt;/code&gt; where &lt;code&gt;bul&lt;/code&gt; can be followed by &lt;code&gt;b&lt;/code&gt; or &lt;code&gt;g&lt;/code&gt; etc.&lt;/p&gt;

&lt;p&gt;After this, and after playing around with a sensible value for the length of the context, I got some better sounding names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;turne
vespichu
ursarill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the size of the context increased, it produced actual Pokémon names with greater and greater frequency. Above 5 all the names produced were actual Pokémon, in machine learning this is called over-training.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing
&lt;/h2&gt;

&lt;p&gt;With testing real AI/ML applications, the system is given half of the training or classification data, and the other half is used to test the classification/prediction accuracy. I could do that with my generator.&lt;/p&gt;

&lt;p&gt;If I gave the generator half the available Pokémon names, could it generate the other half?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;actual_pokemon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;corpus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pokemon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shuffle&lt;/span&gt;

&lt;span class="n"&gt;mid_point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual_pokemon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;training_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actual_pokemon&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;mid_point&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;test_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actual_pokemon&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mid_point&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;algorithm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Markov&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="n"&gt;training_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;context_length: &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;generated_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_name&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;uniq&lt;/span&gt;
&lt;span class="n"&gt;test_data_names_generated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;test_data&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;datum&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;datum&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&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="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;intersection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generated_names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Test Data Names Generated: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;test_data_names_generated&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, the answer was no. It produced roughly 3 of the test data names for every 5000 names produced. As a classification/prediction engine that would be pretty poor performance. But for a fake name generator, that's not bad.&lt;br&gt;
It actually produces names like "chespin" and "porygon" without ever even seeing them before. I call that a win.&lt;/p&gt;
&lt;h2&gt;
  
  
  Is it a Pokémon?
&lt;/h2&gt;

&lt;p&gt;Can it trick a person? That was the whole point of this after all. To answer that question, I built a quiz app to do just that.&lt;/p&gt;

&lt;p&gt;I produced 10,000 names from the generator. This includes some real and some not real names. The app randomly picks one and which you think it is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://is-it-a-pokemon.herokuapp.com/" rel="noopener noreferrer"&gt;Try it&lt;/a&gt; now!&lt;/p&gt;
&lt;h2&gt;
  
  
  Phonemes
&lt;/h2&gt;

&lt;p&gt;Another extension I wanted to try was to consider phonemes as the base unit of the names, rather than just letters. For this I needed to collect a list of phonemes in the English languge and then break the names into groups of letters based on those phonemes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;pokemon_phonemes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pokemon_names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;this_pokemon_name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;remaining_pokemon_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;this_pokemon_name&lt;/span&gt;
  &lt;span class="n"&gt;this_pokemon_phonemes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remaining_pokemon_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;

    &lt;span class="n"&gt;phoneme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phonemes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;phoneme&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;remaining_pokemon_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phoneme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;this_pokemon_phonemes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;phoneme&lt;/span&gt;
    &lt;span class="n"&gt;remaining_pokemon_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remaining_pokemon_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phoneme&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;end&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;this_pokemon_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;this_pokemon_phonemes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting hash had entries that looked like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"charmander"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"m"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"er"&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;The hope I had with this extension was to be able to treat certain groups of letters that make a sound (like "ch") as if it was a single letter in the Markov Chain. I hoped this, in turn, would restrict the possibilities for each phoneme, creating more realistic suggestions.&lt;/p&gt;

&lt;p&gt;It didn't really make too much of a difference, though.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;You can find the code, both for the name generator and the quiz on Github.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tony-rowan/pokemon-name-generator" rel="noopener noreferrer"&gt;Generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tony-rowan/is-it-a-pokemon" rel="noopener noreferrer"&gt;Quiz&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
