Big O Notation
Why is my program running so slow?
My idea for a useful Dev Tool was to write a ruby gem that analyses your code and automatically generates a domain model for your classes. Pretty Good Idea Right?
Before we look at the code, let me remind you that this is an awful implementation. The only reason I’m writing this blog post is to show some of the interesting code snippets that grew from this project, and to highlight that it’s ok to spike code to understand a problem better. However, that code should never reach production; ideally it should be deleted and test driven from the ground up.
Ok proviso’s noted. Let’s look at some code. I’ve created a barebones model of the boris bikes system to test our system on. It just has two classes, docking stations and bikes:
class DockingStation
def initialize(capacity)
@bikes = []
@capacity = capacity
end
def add_bike
bike = Bike.new
if bike.working?
@bikes << bike
else
raise 'Bike Broken'
end
end
end
class Bike
def initialize
@working = true
end
def working?
@working
end
def report_broken
switch_state
end
def fix_bike
switch_state
end
private
def switch_state
@working = !@working
end
end
Our first challenge is to read in the file, and find the classes in it. We’ll use a command line argument to get the name of the file we want to analyse, which we can access by using ARGV[0]
.
To start we require the file we’re analysing. This loads the classes we want to analyse into memory. We then open the file as a text file, and scan it for any class definitions. This gives us an array of string holding the name of the classes in the file.
Then for each string we look up the actual class object in the ruby memory. These are stored as constants in the Object
object. This gives us an array of all the classes we want to analyse.
require(ARGV[0])
file = File.open(ARGV[0])
@classes = file.read.scan(/class (\w+)/).flatten
file.close
@objects = @classes.map do |className|
Object.const_get("#{className}")
end
Right, we’ve got our class objects. Our next step is to find out what methods and attributes they hold. We can use a few core ruby methods to do this.
When we find the public methods and private methods, we compare the method list to those defined on the Object
class. As our classes will all inherit from Object
, this will leave us with only those methods defined on our class.
def find_attributes(object)
object.instance_variables
end
def find_public_methods(object)
object.public_methods - Object.new.public_methods
end
def find_private_methods(object)
object.private_methods - Object.new.private_methods
end
If we write a little script to display this information in a nice format, we can now pass in our Boris Bikes script and get something like the following:
========================================================
CLASSES:
--------------------------------------------------------
DockingStation
- Attributes:
* @bikes
* @capacity
- Public Methods:
* add_bike
- Private Methods:
--------------------------------------------------------
Bike
- Attributes:
* @working
- Public Methods:
* working?
* report_broken
* fix_bike
- Private Methods:
* switch_state
--------------------------------------------------------
This gives us the information to display the nodes on our domain model. However the real strength of these maps is that display the relationships between nodes. To do this we need to delve deep into ruby.
Our approach for looking at class dependencies is to iterate through all our classes, calling each method in turn. We then record all the methods that are executed due to to this method call.
In this record we look for method calls where the object involved is different from the object we originally called the method on.
To record all method calls we use the Set_Trace_Func. This oft overlooked method takes a proc as an argument and allows us to record everything that ruby does.
Lets set it up to add a record to an array every time its invoked. For every public_method in a node we turn on the recorder, then call the method on the nodes class. We then turn off the recorder. The record contains in order:
node.classname
method
event
file
line
id
classname
Note the first class isn’t necessary the same as the second. Indeed these are the records we are looking for. This is where we run a method, and this invokes another method on a different object.
node.public_methods.each do |method|
set_trace_func proc { |event, file, line, id, binding, classname|
@vertices << [node.classname, method, event, file, line, id, classname]
}
node.classname.method
set_trace_func(nil)
end
Even a really simple program returns a huge number of ruby processes. Here’s a sample from running our Boris Bikes program.
[[DockingStation, :add_bike, "c-return", "DOM_Modeller.rb", 78, :set_trace_func, Kernel], [DockingStation, :add_bike, "line", "DOM_Modeller.rb", 81, :find_vertices, NodeMapper], [DockingStation, :add_bike, "call", "DOM_Modeller.rb", 21, :send, MethodSender], [DockingStation, :add_bike, "line", "DOM_Modeller.rb", 22, :send, MethodSender], [DockingStation, :add_bike, "c-call", "DOM_Modeller.rb", 22, :instance_method, Module], [DockingStation, :add_bike, "c-return", "DOM_Modeller.rb", 22, :instance_method, Module], [DockingStation, :add_bike, "c-call", "DOM_Modeller.rb", 22, :arity, UnboundMethod], [DockingStation, :add_bike, "c-return", "DOM_Modeller.rb", 22, :arity, UnboundMethod], [DockingStation, :add_bike, "line", "DOM_Modeller.rb", 23, :send, MethodSender], [DockingStation, :add_bike, "c-call", "DOM_Modeller.rb", 23, :new, Class], [DockingStation, :add_bike, "c-call", "DOM_Modeller.rb", 23, :initialize, Array], [DockingStation, :add_bike, "c-return", "DOM_Modeller.rb", 23, :initialize, Array], [DockingStation, :add_bike, "c-return", "DOM_Modeller.rb", 23, :new, Class], [DockingStation, :add_bike, "line", "DOM_Modeller.rb", 24, :send, MethodSender], [DockingStation, :add_bike, "c-call", "DOM_Modeller.rb", 24, :new, Class], [DockingStation, :add_bike, "call", "/Users/Tom/Programming/MakersAcademy/WeekNine/DOMTest/TestFiles/TestFile.rb", 3, :initialize, DockingStation], [DockingStation, :add_bike, "line", "/Users/Tom/Programming/MakersAcademy/WeekNine/DOMTest/TestFiles/TestFile.rb", 4, :initialize, DockingStation], [DockingStation, :add_bike, "line", "/Users/Tom/Programming/MakersAcademy/WeekNine/DOMTest/TestFiles/TestFile.rb", 5, :initialize, DockingStation], [DockingStation, :add_bike, "return", "/Users/Tom/Programming/MakersAcademy/WeekNine/DOMTest/TestFiles/TestFile.rb", 6, :initialize, DockingStation], [DockingStation, :add_bike, "c-return", "DOM_Modeller.rb", 24, :new, Class], [DockingStation, :add_bike, "line", "DOM_Modeller.rb", 25, :send, MethodSender], [DockingStation, :add_bike, "c-call", "DOM_Modeller.rb", 25, :instance_method, Module]
From this we want to find elements where the calling and called class are different. For example:
[DockingStation, :add_bike, "call", "/Users/Tom/Programming/MakersAcademy/WeekNine/DOMTest/TestFiles/TestFile.rb", 21, :initialize, Bike]
[DockingStation, :add_bike, "call", "/Users/Tom/Programming/MakersAcademy/WeekNine/DOMTest/TestFiles/TestFile.rb", 25, :working?, Bike]
These two records represent the DockingStation class creating a new bike class, and calling .working?
on it. These represent links between our classes. We now have all the information we need to draw our domain model.
Actually drawing the model using a GUI is a little beyond the scope for this spike, and perhaps something we’ll look at next week. For now we’ll just format and print the information to the terminal.
Let’s turn our little project on our airport model which inspired this program. After combining the files we get the information we’re after:
========================================================
CLASSES:
--------------------------------------------------------
Airport
- Attributes:
* @capacity
* @weather
* @landed_planes
- Public Methods:
* land
* takeoff
- Private Methods:
* capacity
* weather
* landed_planes
* pre_landing_checks
* pre_takeoff_checks
* add_plane_to_airport
* remove_plane_from_airport
* at_airport?
* plane_landed?
* weather_safe?
* airport_full?
--------------------------------------------------------
Plane
- Attributes:
* @landed
- Public Methods:
* landed
* landed?
* land
* takeoff
- Private Methods:
--------------------------------------------------------
Weather
- Attributes:
- Public Methods:
* check_safe?
- Private Methods:
--------------------------------------------------------
DEPENDENCIES:
--------------------------------------------------------
- The method call Airport.land calls the method #check_safe? on the Weather class.
- The method call Airport.land calls the method #landed? on the Plane class.
- The method call Airport.land calls the method #land on the Plane class.
- The method call Airport.takeoff calls the method #check_safe? on the Weather class.
- The method call Airport.takeoff calls the method #landed? on the Plane class.
- The method call Airport.takeoff calls the method #takeoff on the Plane class.
========================================================
Leave a Comment