Getting a count of the rows in all of your ActiveRecord models

Warning: Going through ActiveRecord is likely not something you want to do in production unless you have a replica database that your script or your model can point at. See the second part for MySQL and PostgreSQL examples in SQL.

How do you enumerate your models?

For apps created under Rails 5 app or later, all of your ActiveRecord models should be descendants ApplicationRecord as well. If this is an option, you’ll be able to filter out some noise from descendants such as SchemaMigration, etc…

Using the descendants of ApplicationRecord you can map the statistics… You’ll need to catch errors on tables that might only be in production-like environments if you’re running locally:

counts = ApplicationRecord.descendants.map do |klass|
  [klass.name, klass.table_name, klass.count]
rescue Mysql2::Error, ActiveRecord::StatementInvalid # if certain tables aren't present in certain environments
  nil
end


=> []

Oops. I’m in development so… no eager loading of the models.


Rails.application.eager_load! # if in development

counts = ApplicationRecord.descendants.map do |klass|
  [klass.name, klass.table_name, klass.count]
rescue Mysql2::Error, ActiveRecord::StatementInvalid # if certain tables aren't present in certain environments
  nil
end

# lots of SELECT COUNT(*) logs if you're on :debug level logging
# followed by an array of name/table name/counts

You might also want to compact that result due to nils:


Rails.application.eager_load!
counts = ApplicationRecord.descendants.map do |klass|
  [klass.name, klass.table_name, klass.count]
rescue Mysql2::Error, ActiveRecord::StatementInvalid # if certain tables aren't present in certain environments
  nil
end

From there, you can export to CSV:

Rails.application.eager_load!

counts = ApplicationRecord.descendants.map do |klass|
  [klass.name, klass.table_name, klass.count]
rescue Mysql2::Error, ActiveRecord::StatementInvalid # if certain tables aren't present in certain environments
  nil
end

output_filename = "/tmp/counts.csv"

CSV.open(output_filename, "wt") do |csv|
  csv << ['class name', 'table name', 'row count']
  counts.compact.each do |row|
    csv << row
  end
end

What about from the database itself?

The below options are *significantly* faster than going through the Rails stack, so unless you need additional information from Rails, you should refine the below options instead (especially for production)

  • MySQL:
select table_name, sum(table_rows) from information_schema.tables where table_schema = 'app_dbname_development' group by table_name;
select table_schema, 
       table_name, 
       (xpath('/row/cnt/text()', xml_count))[1]::text::int as row_count
from (
  select table_name, table_schema, 
         query_to_xml(format('select count(*) as cnt from %I.%I', table_schema, table_name), false, true, '') as xml_count
  from information_schema.tables
  where table_schema = 'public' --<< change here for the schema you want
) t order by 3 DESC


Self-Modifying Code on a Commodore VIC-20

Note: All code listings are in lower case so that they are pastable into the VICE emulator. Otherwise, you will get graphics/uppercase PETSCII characters on paste.

Examining the structure of how the BASIC code is stored

User program RAM is in locations 4096 to 7680 (decimal) on a VIC 20. The storage format of the basic programs can be dumped with the following BASIC:

for i=4096 to 7680 - fre(1): ? i,chr$(peek(i));peek(i): next i 

I’ve taken the extra step up adding a slightly more sophisticated version of the above at line 10000 in the below code so that I can RUN 10000 to dump memory locations with paging and skipping control and non-printable characters.

10 print "hi"
20 n=peek(4104)
30 x=peek(4105)
40 if n >= 90 then n=65
50 n=n+1
60 x=int(26*rnd(1)+65)
70 poke 4104,n
80 poke 4105,x
90 goto 10
9999 end
10000 b=4096:i=b
10010 e=7680-fre(0)
10020 c=0
10030 ls=20
10040 ? i,
10050 ch=peek(i)
10060 ? ch;
10070 if(ch>=32 and ch<=127)or(ch>=160 and ch<=254)then ? chr$(ch);
10075 ?
10080 if c>ls then ? "continue";: input wt$: c=0
10090 c=c+1
10100 i=i+1
10110 if i>e then end
10120 goto 10040
User program RAM dump

You’ll notice in the above that we start with a null character (0) followed by 12, 16, 10 and 0. 12 and 16 are a pointer to the the memory location of the next line of code (in “little endian” order, so 16 * 256 + 12 = 4108)

The next bytes, at location 4099 and 4100, are 10 and 0. This is the line number for that line of code (again, in little endian format).

Once you get past these 2 2 byte numbers, you have a code…. 153: 153 is the VIC 20 BASIC Keyword Code for the PRINT statement. All syntactically significant tokens (keywords and symbols) are reduced to a single byte (and TAB and SPC functions actually include their left parenthesis as part of this code). The VIC-20 Programmer’s Reference Guide lists out these values (some of these are just their PETSCII codes if individual characters):

VIC 20 BASIC Keyword Codes

You’ll notice that space (32) and double quote (34) are explicitly expressed, as are the individual digits of any number literals.

At the very end of the line is a 0/null again to terminate the line. (Fun part of this experiment: Setting a byte in the middle of the line to 0 makes the rest of the line unreadable by the BASIC interpreter!)

Modifying the code

For an easy first attempt at this, I’m going to just change location 4105 and 4106, which are the letters in HI

10 print "hi"
HI at 4104 and 4105

In the below code, I’m cycling the original H through the alphabet (65-90) and setting the original I with random values:

20 n=peek(4104)
30 x=peek(4105)
40 if n >= 90 then n=65
50 n=n+1
60 x=int(26*rnd(1)+65)
70 poke 4104,n
80 poke 4105,x
90 goto 10
The changing 2-letter strings from the self-modifying code

If you BREAK out of the program (Esc key in VICE emulator) after running and list the first few lines, you’ll see that the initial PRINT statement’s string has indeed changed:

The print statement has had its string changed.

What’s Next?

This is obviously a very trivial exercise of self-modifying code, but any modifications that require anything aside from 1:1 in-place replacement requires more planning: The lines of a program are variable in length, which means that inserting code requires shifting subsequent code in memory. Also, shifting code in memory requires updating all pointers that pointed to the original locations. The next exercise will probably be adding code to the end of the program rather than trying to insert it in the middle.


trs80gp emulator (Model 16 emulation) – saving/reloading your work from BASIC

Setting up a fresh disk

If you’re already in BASIC, just open another trs80gp emulator to create the new disk first.

  • Insert a new disk by going to Diskette -> :1 <empty> -> <<unformatted dmk>>
  • FORMAT 1 and “Y” when asked to Continue?
Formatting progress
  • Once formatting completes select Diskette -> :1 * <<unformatted>> -> Export… -> [fill out “File name:” field] -> [Save]
  • Now load the saved disk with Diskette -> :1 * <<unformatted>> -> Replace… -> [select the disk file you just created] -> [Open]
  • DIR 1 should show the empty disk in drive 1

Saving your work

  • Compose your program in BASIC or BASICG (graphics BASIC)
  • The filespec of TRSDOS-II files is filename/ext.password:drive_number (if you’re used to DOS 8.3 names or Windows, your habitual “extension” would be a “password” in TRSDOS-II)
  • To save a program named “COS” with a “BAS” extension on Drive 1, type SAVE "COS/BAS:1"
  • SYSTEM to exit basic.
  • DIR 1 should should the directory of your disk with the COS/BAS showing. In my example below, I also tried to save “COS.BAS” which is actually “COS” with a password of “BAS”
DIR command output

Loading your work

  • You can PRINT or DUMP your saved file with the COS/BAS filespec as before, but it will be a somewhat binary output and not plain text like you might expect from modern programming language files.
  • Ultimately, you will have to go back to BASIC to reload with the same filespec (LOAD "COS/BAS:1“)

Resources:


Is Solar Worth It? A Florida Panhandle Perspective

If you’re considering solar, you can find solar installers via EnergySage (referral link) and get a $25 Amazon Gift Card when you go solar. I found the installer that we ultimately used, which was a lot better option than using one of the many door-to-door solicitors that show up in the neighborhood.

Perspective on our Placement for Solar

Our house is in Cantonment, FL, which is at 30.6°N latitude. If you’ve been to the Florida beaches in early spring after spending the winter somewhere [reasonably north of Florida], your skin is well aware of how much more intense the sun is. Aside from cities in Florida, only San Antonio, Austin, and Houston in Texas, New Orleans in Louisiana, and Mobile in Alabama are further south than we are.

The back roof of our house also points pretty much due south, and as you can see, the south panels are the most productive through the day:

Lifetime energy generation after about 4 years, per panel.

Specs on our system

We have 48 290W panels with micro inverters on each panel so that each panel is monitorable. They were definitely more expensive this way and based on outage turnaround time that prompted me to write a script to monitor, it would probably cost $200-400 worth of lost production waiting to fix the problem. Given that that’s about 1% of the cost of the solar array and the price differential was easily 10-20% the extra “observability” into individual panels seems to be too steep a premium (but the more traditional panels also had the aluminum storm door look versus the fancy new ones!)

We also have a solar collector for hot water (that also requires that we stick with electric for the water heater.) The tank is 80 gallons, with half of the tank always heated by electric as a backup. The solar collector will heat up the other half by circulating to between the tank and the roof.

Unlike the photovoltaic panels, the solar part of the water heater doesn’t produce usable hot water on its own in the winter months. It will, however, warm up enough during the day to assist in heating by supplying 80-90℉ water to the electric side. This will be offset by the fact that if it gets near or below freezing, the water in the tank has to circulate to the collector on the roof so that it doesn’t freeze. That’s not a huge problem in the winter here, because it collects enough warm water on those days to get through the night… but for places that get significant snows *plus* power outages, that could be a recipe for disaster.

Pricing

At the time, the solar installs were financed at 100%, with the option of either financing it all at a high single digit rate or 30% down via 12 months same as cash in anticipation of the tax credit money and 70% on a 12 year loan at low single digit APR. The full financing at the higher rate was pretty much like borrowing money on your future utility bills at a high interest rate in order to keep the tax credit.

87% of the solar loan we have goes to paying off the solar panels (vs. the water heater), so about $235/month.

Return on Investment

Energy Generation Report

According to our energy bills, the charge for electricity is about $0.14/kWh. This includes fees that move up and down in more of a step-like manner, but not fixed charges. At 18MWh/year that means we’re generating about $210/month in electricity vs. the $235/month payment. There are two things to keep in mind: First, the payments only last 12 years, but the solar panels are supposed to be at at least 80% efficiency for 20. Second, the return on investment improves if the price of energy increases faster than the efficiency of the solar cells decays.

Verdict

Solar is not a no brainer. If you want to do it for the environment, that might put you over the top in terms of return on investment. The solar water heater (left out of this post) is a lot harder to measure, but you probably want to live in a place that doesn’t get near freezing in the winter.


Raspberry Pi Zero W to monitor Enphase Envoy Solar Array

I decided to set up some form of monitoring for my solar installation after a fuse and the breaker panel broke down leaving me without solar generation for a couple stretches during near-peak, up to about 1,400 kWh, or about $140-210 worth of solar generation.

Missing output

Components (physical and software)

  • A RaspberryPi Zero W on the same wireless network as the Envoy controller was set up on (initially used PiBakery to configure hostname/wifi/username/password, but the project is a little bit stale at this point).
  • A Nexmo account (part of Vonage APIs now) to allow for SMS alerts on zero output when the sun is up.
  • RubySunrise for only emailing alerts from dusk until dawn.
  • Ruby Gmail and a Gmail account for email informational “down” alerts just to be aware that the cron job is running.
  • cron and gmail

Connections

These are described in the source code repo as well

  • ENVOY_HOST for me was envoy.local, but depending on your DNS situation, your mileage may vary. I got my local DNS in a weird enough state that I just looked up the envoy.local IP on my wireless router’s status page and used that.
  • USERNAME and PASSWORD are the Gmail username and app-specific password credentials I generated for the gmail account I used.
  • INVERTER_COUNT is compared to the number of inverters you should have so that even if the array is producing, you can still generate an error if one of them isn’t reporting (only valid when producing)
  • LATITUDE and LONGITUDE plucked from a site that displays your geolocation… this, along with your TZ represented in a form within the TZInfo::Timezone list, and RubySunrise allow you to figure out if the sun’s up.
  • NEXMO* are api keys and config from the Nexmo site (NEXMO_SMS_TO is your personal mobile to alert to)
  • TO_EMAIL is the email to actually mail to

Code

.config must be of the form that follows but the rest of the code can be cloned from envoy-rpi-zero-monitor

    USERNAME='some.burner.gmail.account'
    PASSWORD='gmai1@cc0untp@$$w0rd'
    TO_EMAIL='an.email.you.read@example.com'
    NEXMO_API_KEY="3ab3789123"
    NEXMO_API_SECRET="123456sSD8dh"
    NEXMO_SMS_FROM="19281123581"
    NEXMO_SMS_TO="15551112222"
    LATITUDE=20.1237899
    LONGITUDE=-57.3364631
    TZ='America/Chicago'
    ENVOY_HOST='192.168.1.222'
    INVERTER_COUNT=100
# crontab runs every hours and inits rbenv to use the right ruby version because
# I didn't really care about "production readiness"... it's a Raspberry Pi Zero W
0 * * * * cd /home/twill/envoy-rpi-zero-monitor && eval "$(rbenv init -)" && ruby read-envoy.rb

YMMV

This all depends on having an Enphase Enlighten Envoy (and a bunch of other random “E” names) as your solar monitor, but if you have a relatively recent solar install and your technician needed to configure the monitor for your wifi, then you probably have a similar device with a pollable endpoint. Look at your wireless router’s web console and you’ll see that monitor:

If you browse to that name or the IP address associated, you’ll probably get a web page with status. If you reload with the network tab up, you’ll probably see it retrieve the data via a .json endpoint:

From there, you can build your own monitor around it.


Referencing one trait from another trait in factory_bot

Sometimes you want to DRY up traits by referencing one trait from another trait in factory_bot. I tried searching on “inheriting traits” (that’s just for one factory inheriting traits from another and was in a factory_bot issue in GitHub). I accidentally stumbled upon the answer in a slightly unrelated StackOverflow question about calling a trait from another trait with params in factory_girl.

Ultimately, you use the trait name from the first trait as a method invocation in the referencing trait:

FactoryBot.define do
  factory :user do
    role
    trait :with_supervisor do
      # complex set up might go
      # here
      after(:create) do |user|
        supervisor { create(:user) }
      end
    end
    trait :with_organization do
      with_supervisor # invoke the other trait first
      organization
    end
  end
end

RAW_POST_DATA in rspec rails for Rails 5.2 and beyond

The last time I was trying to specify RAW_POST_DATA in rspec was probably Rails 3 or 4, but I ran into a situation trying to test an edge case for error handling where I wanted that same functionality. I quickly found this issue [Unable to POST raw request body], but didn’t immediately figure out what wasn’t being set correctly.

In this case the test setup I was using was setting multipart/form-data instead of application/xml on the content types:

{:HTTP_ACCEPT=>"application/xml", :HTTP_CONTENT_TYPE=>"multipart/form-data", :CONTENT_TYPE=>"multipart/form-data"}

Because of this, the Rails controller tests that rspec hooks into was trying to break following malformed xml down to parameters:

            <test>
              <data&nbsp;
              <![CDATA[THIS|IS|SENSITIVE|BUT|MALFORMED]]>
              </data>
            </test>
 Minitest::Assertion:
   Expected response to be a <400: bad_request>, but was a <422: Unprocessable Entity>
   Response body: <errors>
       <error>["<test>\n  <data", "nbsp;\n  <!"] are not permitted parameters</error>
   </errors>

I finally noticed that the mime-type might be involved. In this code, Content-Type was also an issue, so:

  • Removed HTTP_CONTENT_TYPE from the headers
  • Set CONTENT_TYPE header to 'application/xml' instead of 'multipart/form-data' to prevent automatic params parsing in this case.
  • Passed as: :xml into the test to get the 'mime-type' correct.

Ultimately, if your code hasn’t boxed you in, then the as: :xml and passing raw data as a parameter should work:

post things_path, params: raw_xml_data, headers: non_form_data_headers, as: :xml

## replacement for the following:
# @request.env['RAW_POST_DATA'] = raw_xml_data
# post things_path

Tandy/Radio Shack TRS-80 Model 16 Graphics

TRS-80 Model 16 Nostalgia

I grew up with a TRS-80 Model I with Level 2 Basic and a TRS-80 Model 16 with 128K of RAM that were our first computers in the house prior to me getting my first computer, a Commodore 128. There was a book about Level 2 Basic that came with the TRS-80 Model I that was (as far as I know) a giveaway, but I never saw the TRS-80 Model I running except when it was in my grandmother’s house in Old Louisville on the 3rd floor when my dad’s younger brothers were playing around with a simple number guessing game program.

One thing that intrigued me about the TRS-80 Model I was its very simple graphics commands, SET and RESET which operated on a 48×128 (row x column) graphics grid as well as PEEK and POKE commands which could address a 2×3 grid of those same graphics grid points, but on the 16×64 character grid.

BASICG? What is this

I played around with the Model 16 and its 8″ disks and BASIC in there, but it wasn’t until I stumbled on PDF documentation for the Tandy TRS-80 model II that I discovered that the business computers (Model-II / Model-12 / Model-16) actually supported graphics at all. Looking at the Model_2_Computer_Graphics_26-4104.pdf scanned reference, it turns out BASICG (must be all uppercase in TRS-DOS) is the way to load the BASIC graphics interpreter.

I’m not sure if the TRS-DOS disks that came with the Model 16 my dad had had this interpreter, but the trs80gp emulator includes BASICG on the default boot disk for Model 16 mode:

What’s this BASICG on the TRSDOS disk?

(It wasn’t until after the TRS-80 Model 16 was gone that I learned about the DIR command via using MS-DOS, but that would have been helpful for digging in more… Ironically, my Commodore 128 supported a DIRECTORY command, so … SO CLOSE BUT SO FAR)

The TRS-80 Model 16 screen is 640×240 (x,y) addressable monochrome pixels. Unlike the Model I with Level 2 BASIC, BASICG has some more advanced commands… LINE, CIRCLE, PAINT… also GET to read the contents of the screen and that can later be PUT.

Interestingly enough, although the CIRCLE documentation say that the third argument is “r”/radius

CIRCLE command documentation

actual usage in the emulator appears to behave like the argument is a diameter instead:

Circles drawn on text

I’m definitely looking for more resources on the Model 16, so if you have background, manuals, etc., email me at thomas at thisdomain.


String#tr in ruby (like tr in Linux) complete with figuring out slashes.

It seems like I’ve seen quite a few programming puzzles in the last few weeks that involved translating mistyped input in which the hands were shifted (right) on the keyboard. My first thought was the tr utility in *nix operating systems, but didn’t immediately go looking for or notice that ruby has a tr method on string. However, after doing a trivial implementation involving keyboard rows like the following, I stumbled on the tr method.

  # initial array of characters/strings to shift back to the left with [index-1]
  KEYBOARD_ROWS= [
    '`1234567890-=',
    'qwertyuiop[]\\', # need to escape the backslash or else debugging pain
    "asdfghjkl;'", # double-quotes here because single quote embedded
    'zxcvbnm,./',
    '~!@#\$%^&*()_+',
    'QWERTYUIOP{}|',
    'ASDFGHJKL:"',
    'ZXCVBNM<>?'
  ].join

Attempting to rewrite this for .tr presented a few challenges, however. If you are substituting for \, -, or ~, you have to escape the characters. You also have to escape them from their string representation, which makes for some head-spinning levels of escaping (zsh users who run shell commands through kubectl might be familiar with this pain as well):

# puts '\\~-'.tr('\\', 'a') # doesn't match because \ is passed to tr and not escaped
a~-
# puts '\\~-'.tr('\\\\', 'a') # now \\ is passed to tr, which is
a~-
# puts '\\~-'.tr("\\\\\\", 'a') # with double quotes, you need an extra pair, for 6 total.
a~-
# puts '\\~-'.tr('\\~', 'b') # the escaping backslash needs to be doubled
\b-
# puts '\\~-'.tr("\\\~", 'b') # the escaping backslash needs to be tripled
\b-
# puts '\\~-'.tr('\\-', 'c') # the escaping backslash needs to be doubled
\~c
# puts '\\~-'.tr("\\\-", 'c') # the escaping backslash needs to be tripled
\~c

So if you’re going to use translate to “shift” hands back to the left, the two arguments to tr, SHIFTED_KEYBOARD_ROWS and UNSHIFTED_KEYBOARD_ROWS would have to be defined with the following escaping:

  SHIFTED_KEYBOARD_ROWS =
    [
      '1234567890\\-=',
      'wertyuiop[]\\\\', # 4x backslash = backslash
      "sdfghjkl;'",
      'xcvbnm,./',
      '!@#\$%\^&*()_+',
      'WERTYUIOP{}|',
      'SDFGHJKL:"',
      'XCVBNM<>?'
  ].join

  UNSHIFTED_KEYBOARD_ROWS= [
    '`1234567890\-',
    'qwertyuiop[]', # need to escape the backslash or else debugging pain
    'asdfghjkl;',
    'zxcvbnm,.',
    '~!@#\$%\^&*()_',
    'QWERTYUIOP{}',
    'ASDFGHJKL:',
    'ZXCVBNM<>?'
  ].join

  def self.translate(string)
    string.tr(SHIFTED_KEYBOARD_ROWS, UNSHIFTED_KEYBOARD_ROWS)
  end

Tracing / Debugging ruby output like set -x in bash

In writing some shell scripts in ruby, I decided that I needed to be able to debug (trace) the lines that were being executed. I even ran across a closed StackOverflow question looking for the same thing.

code=ARGF.readlines.grep_v(/^$/)
eval code.map { |c| %Q|puts "+ #{c.gsub("\"", "\\"").strip}"| }.zip(code).join($/)

After playing around with one of the other answers (see above), I ended up taking a different tactic to try and figure out how to debug the scripts. (By the way, the above code breaks if you have line breaks in a single statement, like the following contrived example):

y = 2
      + 4

The important search term here is “trace”, or Tracer to be exact.

Take the following example:

bind = binding
p bind

bind.local_variable_set(:bind, 2)

p bind

p binding.local_variables

bind = binding

p eval("bind", bind)

if 2 > 3
  puts 2
else
  puts 3
end


if true
  puts "looks like the if true is compiled out"
end

if you run the above (contained in a filename binding.rb) using ruby -r tracer binding.rb then you get the following:

#0:/home/tpowell/.rbenv/versions/2.5.5/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:Kernel:<:       return gem_original_require(path)
#0:binding.rb:1::-: bind = binding
#0:binding.rb:2::-: p bind
#<Binding:0x000055fe6879cd38>
#0:binding.rb:4::-: bind.local_variable_set(:bind, 2)
#0:binding.rb:6::-: p bind
2
#0:binding.rb:8::-: p binding.local_variables
[:bind]
#0:binding.rb:10::-: bind = binding
#0:binding.rb:12::-: p eval("bind", bind)
#0:binding.rb:10::-: bind = binding
#<Binding:0x000055fe68835718>
#0:binding.rb:14::-: if 2 > 3
#0:binding.rb:17::-:   puts 3
3
#0:binding.rb:22::-:   puts "looks like the if true is compiled out"
looks like the if true is compiled out

One interesting difference between how ruby -r tracer works from set -x is that the ruby tracer appears skips evaluating the if true at all. The above runs were against ruby 2.5.5. Looking at 3.0.0 (and as far back as 2.6.x), I only get the output of the script:

#<Binding:0x000055af54e0c730>
2
[:bind]
#<Binding:0x000055af54e0c050>
3
looks like the if true is compiled out

Looking at Tracer, it’s using set_trace_func under the hood:

set_trace_func proc { |event, file, line, id, binding, classname|
  printf "%8s %s:%-2d %10s %8s\n", event, file, line, id, classname
}

Adding that in the 2.6.x+ world returns:

c-return binding.rb:1  set_trace_func   Kernel
    line binding.rb:5
  c-call binding.rb:5     binding   Kernel
c-return binding.rb:5     binding   Kernel
    line binding.rb:6
  c-call binding.rb:6           p   Kernel
  c-call binding.rb:6     inspect   Kernel
c-return binding.rb:6     inspect   Kernel
.
.
.

That output can be filtered by the event type , but the lines of code themselves aren’t output and apparently set_trace_func was apparently obsoleted as of 2.1.10. TracePoint is the updated way to accomplish this:

trace = TracePoint.new(:line) do |tp|
  p tp
end
trace.enable
.
.
.

But we still have the same problem:

#<TracePoint:line@binding_trace_point.rb:8>
#<TracePoint:line@binding_trace_point.rb:9>
#<Binding:0x00005652f2a125e8>
#<TracePoint:line@binding_trace_point.rb:11>
#<TracePoint:line@binding_trace_point.rb:13>
2
#<TracePoint:line@binding_trace_point.rb:15>
[:trace, :bind]

A crude solution I’ve found around this is to read the line from the file mentioned in the TracePoint from within the block (and this apparently doesn’t end up with a stack overflow).

trace = TracePoint.new(:line) do |tp|
  puts "+ #{File.open(tp.path) { |f| f.each_line.to_a[tp.lineno-1] }}"
end

trace.enable
.
.
.

Which produces a somewhat set -x output:

+ bind = binding
+ p bind
#<Binding:0x000055ef6c9d9f70>
+ bind.local_variable_set(:bind, 2)
+ p bind
2
+ p binding.local_variables
[:trace, :bind]
+ bind = binding
+ p eval("bind", bind)
+ bind = binding
#<Binding:0x000055ef6c8321b8>
+ if 2 > 3
+   if 3 > 2
+     puts 3
3
+   puts "looks like the if true is compiled out"
looks like the if true is compiled out