At AppLand we're currently working on ways in which we can visualize how changes to the code affect program design and execution. But it turns out you don't need any fancy visuals to be able to tell something useful.
(BTW, see the whole code gallery!)
If you're not familiar with AppLand, at the very basic level, it allows you to record program execution traces. These traces can then be processed to surface interdependencies between code artifacts (such as classes, objects and functions), creating a visual map of a piece of software, how it's designed and how it operates. Go check out https://app.land if you haven't already -- there are plenty of examples of open source software to explore!
However, let's step back and focus on the core capability: execution traces. What if we could tell how a specific patch affects the execution before even reading and understanding the change?
For example, say that Acme Corp has a very important module, fizzbuzz.rb:
module FizzBuzz
module_function
def fizz? i
i % 3 == 0
end
def buzz? i
i % 5 == 0
end
def fizzbuzz i
if fizz?(i) && buzz?(i)
return 'fizzbuzz'
end
if fizz?(i)
return 'fizz'
end
if buzz?(i)
return 'buzz'
end
return i
end
end
It's executed like here, in test.rb:
require 'fizzbuzz'
puts (1..36).map(&FizzBuzz.method(:fizzbuzz)).join ' '
Now a new employee wants to impress the management with her mad coding skills, and says she can vastly improve performance by optimizing the module. Here's what she came up with:
module FizzBuzz
module_function
def fizz? i
i % 3 == 0
end
def buzz? i
i % 5 == 0
end
def fizzbuzz i
matches = [fizz?(i), buzz?(i)]
return i unless matches.any?
[matches[0] ? 'fizz' : '', matches[1] ? 'buzz' : ''].join
end
end
For the sake of the argument imagine it's a subtle change in a larger codebase. Can we tell how the execution changed?
We'll first use appmap-ruby to record the execution. Annoyingly for such a one-off it insists on having a config file, but it's not too difficult to make it happy by creating appmap.yml:
name: fizzbuzz
packages:
- path: fizzbuzz.rb
Now we can record the execution (note you probably need to set RUBYLIB=.
if you're following at home so Ruby can find
fizzbuzz.rb
):
$ appmap record -o fizz.json test.rb
$ cat fizz.json
{"version":"1.2","metadata":{"app":"fizzbuzz","language":{"name":"ruby","engine"
:"ruby","version":"2.5.5"},"client":{"name":"appmap","url":"https://github.com/a
pplandinc/appmap-ruby","version":"0.37.0"}},"classMap":[{"name":"fizzbuzz.rb","t
ype":"package","children":[{"name":"FizzBuzz","type":"class","children":[{"name"
:"fizzbuzz","type":"function","location":"fizzbuzz.rb:13","static":true},{"name"
:"fizz?","type":"function","location":"fizzbuzz.rb:5","static":true},{"name":"bu
zz?","type":"function","location":"fizzbuzz.rb:9","static":true}]}]}],"events":[
{"id":1,"event":"call","thread_id":47377370105340,"defined_class":"FizzBuzz","me
thod_id":"fizzbuzz","path":"fizzbuzz.rb","lineno":13,"static":true,"parameters":
[{"name":"i","class":"Integer","object_id":3,"value":"1","kind":"req"}],"receive
...
Ok, not terribly illuminating. We'd like to record the execution after applying the change and compare, but this wall of JSON doesn't seem to inviting. But let's take a closer look:
$ jq '.events[0]' fizz.json
{
"id": 1,
"event": "call",
"thread_id": 47377370105340,
"defined_class": "FizzBuzz",
"method_id": "fizzbuzz",
"path": "fizzbuzz.rb",
"lineno": 13,
"static": true,
"parameters": [
{
"name": "i",
"class": "Integer",
"object_id": 3,
"value": "1",
"kind": "req"
}
],
"receiver": {
"class": "Module",
"object_id": 47377376841220,
"value": "FizzBuzz"
}
}
Now we're getting somewhere? We don't care about most of these if we only want to compare traces. So let's transform it to just what we need:
$ jq -r '.events[] | [.event, .method_id] | join(" ")' fizz.json | tee fizz1.trace
call fizzbuzz
call fizz?
return
call buzz?
return
return
call fizzbuzz
call fizz?
return
call buzz?
...
Cool! Let's apply the patch and repeat the recording and transformation to get fizz2.trace:
patch -p1 < newfizz.patch
appmap record -o fizz2.json test.rb
jq -r '.events[] | [.event, .method_id] | join(" ")' fizz2.json > fizz2.trace
diff -u fizz[12].trace
Lo and behold:
--- fizz1.trace 2020-10-21 15:25:14.744000000 +0200
+++ fizz2.trace 2020-10-21 15:25:10.716000000 +0200
@@ -1,16 +1,12 @@
call fizzbuzz
call fizz?
return
-call fizz?
-return
call buzz?
return
return
call fizzbuzz
call fizz?
return
-call fizz?
-return
call buzz?
return
return
@@ -19,22 +15,16 @@
[...repeats 11 more times...]
It does improve performance! It saves a bunch of calls to fizz? which obviously is a very expensive method. LGTM!
(As an aside, note it also immediately reveals something which might not be clear at the first glance at the code: we're not saving any buzzes, just fizzes.)