Using lumitrace to eliminate redundant type conversions in Ruby
Source: Dev.to
Using lumitrace to eliminate redundant type conversions in Ruby
When writing Ruby, it’s tempting to sprinkle .to_s, .to_i, and .to_sym calls “just in case.” Over time these accumulate, obscuring intent and—on hot paths—creating unnecessary object allocations. I used lumitrace to systematically find and remove these redundant conversions from RuVim, a Vim‑like editor written in Ruby.
What is lumitrace?
lumitrace records runtime values (types, counts, etc.) for each Ruby expression. With --collect-mode types it outputs JSON showing how many times each expression returned each type.
lumitrace --collect-mode types -j exec rake testOne command gives you a full type profile of your test suite.
What it revealed
For example, window.rb had this setter:
def cursor_x=(value)
@cursor_x = value.to_i
endlumitrace showed that value was 100 % Integer. All callers passed an Integer, so the .to_i was pure waste.
Similarly, keymap_manager.rb called mode.to_sym 31,341 times during the test run, but mode was always a Symbol. Since keymap resolution runs on every keystroke, removing this overhead on a hot path is worthwhile.
The process
- Run
lumitrace --collect-mode types -j exec rake testto collect type data. - Extract
.to_s,.to_i,.to_sympatterns from the JSON and identify cases where the receiver was always the target type. - Verify by checking all callers—only remove when confirmed safe.
- Run the test suite to confirm everything passes.
Result: ~50 redundant type conversions removed across 9 files.
| Conversion | Approx. removed | Key locations |
|---|---|---|
.to_s | ~25 | editor, completion_manager, dispatcher, global_commands |
.to_i | ~15 | window, screen, text_metrics, app |
.to_sym | ~15 | keymap_manager, editor, buffer, key_handler |
From type inconsistency to better design
Beyond removing redundant conversions, type inconsistencies can reveal design issues. lumitrace records the frequency of each type per expression. A single type indicates stability; a mix suggests a potential problem.
The bang parameter in CommandInvocation illustrated this. lumitrace showed a mix of NilClass, FalseClass, and TrueClass:
def initialize(id:, argv: nil, kwargs: nil, count: nil, bang: nil, raw_keys: nil)
@bang = !!bang
endThe default nil was coerced to a boolean with !!, but bang semantically represents “whether the Ex command has a ! suffix”—a flag that should be false by default, not “unspecified.” The fix:
def initialize(id:, argv: nil, kwargs: nil, count: nil, bang: false, raw_keys: nil)
@bang = bang
endThis change wasn’t just about removing a conversion; the type profile highlighted inconsistent usage, prompting a cleaner design.
What I kept
Not every conversion should be removed. The following were intentionally preserved:
- String slicing results (
line[0...idx].to_s) — can returnnil. - External input boundaries (
effective_option(...).to_i) — user settings may be strings. - String‑to‑number parsing (
m[1].to_i) — regex match results are strings. - APIs accepting both Symbol and String (
spec_call.to_sym) — by design.
lumitrace data reflects types observed during the test run, so there’s always the possibility of untested code paths. Caller verification remains essential.
Takeaway
Having a type profile makes the question “is this .to_s necessary?” dramatically easier to answer. Without it, you’d need to trace callers through the codebase by hand. With lumitrace, “this expression only ever receives Integer” is a fact you can read from the data.
Ruby’s dynamic typing makes runtime type information especially valuable. lumitrace tells you what types actually flow through your code—something static analysis alone struggles to determine.
- lumitrace:
- RuVim:
- Changes from this work: