Pivotal Labs

Main menu

Skip to primary content
Skip to secondary content
  • About
  • Case Studies
  • Team
    • Executives
    • Locations
      • San Francisco (HQ)
      • Boston
      • Boulder
      • Denver
      • London
      • Los Angeles
      • New York
  • Community
    • Blogs
    • Tech Talks
    • Events
  • Careers
    • Lifestyle
    • Principles & Practices
    • Benefits
    • FAQ
    • Apply
  • Contact
    • Press Room
    • Press Releases
    • In The News
    • Press Kit
  • All
  • Labs
  • Standup
  • Tracker
John Barker

All evidence points to OOP being bullshit

John Barker
Thursday, February 21, 2013

This is the second part in a series I’m writing about lessons that can be learned from functional programming. Find the first part here.

Object Oriented Programming (OOP) as an idea has been oversold. The most commonly used languages in use today are designed around the idea of OOP. Extremist languages like Java force you to think of everything in terms of objects. But is Object Orientation (OO) a good idea? Does it have problems? Is it the right tool for everything? Let’s explore some of these questions in a slightly tongue in cheek and cathartic rant.

Imperative vs Declarative

The object-oriented model makes it easy to build up programs by accretion. What this often means, in practice, is that it provides a structured way to write spaghetti code. — Paul Graham

Procedural programming languages are designed around the idea of enumerating the steps required to complete a task. OOP languages are the same in that they are imperative – they are still essentially about giving the computer a sequence of commands to execute. What OOP introduces are abstractions that attempt to improve code sharing and security. In many ways it is still essentially procedural code.

Declarative languages on the other hand are about describing computation. While a a declarative language necessarily maps down to imperative code, the resulting code often reveals less incidental complexity and can sometimes be much more easily parallelized.

State

The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle. — Joe Armstrong

State is not your friend, state is your enemy. Changes to state make programs harder to reason about, harder to test and harder to debug. Stateful programs are harder to parallelize, and this is important in a world moving towards more units, more cores and more work. OOP languages encourage mutability, non determinism and complexity.

As someone who was initially hostile to the idea that state is the root of all problems, I initially greeted this idea with skepticism. Mutating state is so easy and fundamental in OOP that you often overlook how often it happens. If you’re invoking a method on an object that’s not a getter, you’re probably mutating state.

Nouns and Verbs

Java is the most distressing thing to happen to computing since MS-DOS. — Alan Kay

The typical college introduction to OOP starts with a gentle introduction to objects as metaphors for real world concepts. Very few real world OOP programs even consist entirely of nouns, they’re filled with verbs masquerading as nouns: strategies, factories and commands. Software as a mechanism for directing a computer to do work is primarily concerned with verbs.

OOP programs that exhibit low coupling, cohesion and good reusability sometimes feel like nebulous constellations, with hundreds of tiny objects all interacting with each other. Sacrificing readability for changeability. Many of  OOP best practices are in fact encouraged by functional programming languages.

Inheritance vs composition

Object-oriented programming is an exceptionally bad idea which could only have originated in California — Edsger W. Dijkstra

Inheritance is one of the primary mechanisms for sharing code in an an OO language. But this idea is so problematic that even the keenest advocates of OO discourage this pattern. Inheritance forces you to define the taxonomy and structure of your application in advance, with all it’s connections and intricacies. This structure is resistant to change which is one of the primary problems software developers face every day. It also fails to model some pretty fundamental concepts.

Further reading

This is far from an exhaustive list of the criticisms leveled at OOP. While I believe the problems with OOP are extensive, I do think it is a valuable mechanism for developing software. But is certainly not the only one. The biggest problem in my mind is thus:

When people overcome the significant hurdle of fully appreciating OOP, they tend to apply it to every problem. OOP becomes the solution, and every problem looks like a nail.

There’s got to be a better way…

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
John Barker

Why you should care about functional programming.

John Barker
Wednesday, January 30, 2013

I’ve been experimenting with functional programming (FP) languages for a little while now and their acceptance is generally increasing amongst the wider developer community. This is the first post in a series of articles I hope to do that explore FP, what it is and what we could learn from this trend.

Why now?

Functional programming languages are by no means new. LISP often thought of as one of the first programming languages was developed in 1958. IPL a sort of assembler like symbolic language appeared in 1954. The foundations of FP are embedded in mathematical constructs much older than the concept of an electronic computer.

But for all intents and purposes they sort of disappeared into obscurity, replaced by procedural languages like C or Object Orientated (OO) like C++ or Java. Partially because they were impractical – some of the constructs used by FP languages performed worse than their imperative counterparts. In a time when CPU power was scarce, this was not an option.

A different age

Thanks to Moore’s law this has changed somewhat. Computers now are certainly more powerful then they were when the first LISP interpreter appeared. But the ever scaling MHZ peak has slowed. New CPUs come with more cores, not more cycles. To take advantage of these improvements we need to look to parallelization and FP languages are inherently suited to this kind of work.

I am not alone in recognizing this. IBM released this article recognizing the rising trend. ThoughtWorks has identified Scala and Clojure in its technology radar and recognizes momentum building behind several FP languages: http://www.drdobbs.com/mobile/thoughtworks-eyes-seismic-shift-in-progr/240009675. Spend any time on Hacker News and you’ll regularly see links on the topic.

I feel like FP is an idea whose time has come and I’m very excited by the new languages, ideas and technologies that are appearing. Hopefully Pivotal Labs will someday join me on this journey.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Ken Mayer

TDD Action Caching in Rails 3

Ken Mayer
Wednesday, March 28, 2012

On my current project, we needed to prove that an action cache was working as expected. Alas, the blogosphere had either out-of-date or unhelpful information. So, after many experiments, we came up with an RSpec test that does what we want. It seems ugly to me, and I hope there’s a better way. The names have been changed to protect the guilty. Any resemblances to actual classes and methods are purely coincidental.

We needed to confirm that a certain action was cached. This action is preview in the brands controller. Using the usual Rails url helpers, we construct some fixture data.

describe BrandsController do
  describe "caching" do
    let(:brand) { Factory.create(:brand) }
    let(:preview_cache_path) {'views/test.host' + preview_brand_path(brand)}
  end
end

Then we wrote our first test:

it "should action cache #preview" do
  Rails.cache.clear
  get :preview, :brand_id => brand.to_param
  ActionController::Base.cache_store.exist?(preview_cache_path).should be_true
end

This won’t work at all, however; because, in the test environment, caching is turned off.

$ cat config/environments/test.rb
Activator::Application.configure do
...
  config.action_controller.perform_caching = false

So, we need an around block to temporarily turn caching on:

around do |example|
  caching, ActionController::Base.perform_caching = ActionController::Base.perform_caching, true
  example.run
  ActionController::Base.perform_caching = caching
end

That’s great, but the default cache store is the :null store, which, as its name implies, does nothing.

around do |example|
  caching, ActionController::Base.perform_caching = ActionController::Base.perform_caching, true
  store, ActionController::Base.cache_store = ActionController::Base.cache_store, :memory_store
  example.run
  ActionController::Base.cache_store = store
  ActionController::Base.perform_caching = caching
end

Better. But our tests still won’t run because while ActionController uses the cache_store, Observers and Sweepers
use Rails.cache and that is only updated at boot time.

around do |example|
  caching, ActionController::Base.perform_caching = ActionController::Base.perform_caching, true
  store, ActionController::Base.cache_store = ActionController::Base.cache_store, :memory_store
  silence_warnings { Object.const_set "RAILS_CACHE", ActionController::Base.cache_store }

  example.run

  silence_warnings { Object.const_set "RAILS_CACHE", store }
  ActionController::Base.cache_store = store
  ActionController::Base.perform_caching = caching
end

Did I mention that Rails.cache is an accessor for the global, constant, RAILS_CACHE. Ugh.

So, now, we can implement our method

class BrandsController < ApplicationController
caches_action :preview
  def preview
  end
end

But that is still not enough. caches_action
has an interesting performance enhancement; it doesn’t actually set up the action caching unless caching is enabled at class load time. Since we’re not turning caching on until test time, the caches_action method call in the controller class does nothing. We need to re-add it in our test spec.

it "should action cache #preview" do
  Rails.cache.clear
  BrandsController.caches_action :preview # must be recapitulated to get around load time weirdfullness

  get :preview, :brand_id => brand.to_param

  ActionController::Base.cache_store.exist?(preview_cache_path).should be_true
end

This is ugly; it doesn’t test very much (except the underlying caching module, and why bother testing the framework). At least it proves to ourselves that the action is cached and the cache key is what we expect.

Now that we’ve got caching under control, let’s check cache expiration (using a Sweeper).

it "should clear the cache on #update" do
  ActionController::Base.cache_store.write(preview_cache_path, 'CACHED ACTION')

  put :update, id: brand.to_param, brand: {one: 'attribute', after: 'another'}

  ActionController::Base.cache_store.exist?(sign_up_cache_path).should be_false
end

First, I create a cached object, in this case, just the string ‘CACHED ACTION’ and then I invoke the action, and then, I hope, the cache will be expired.

It doesn’t really matter what happens in the #update method of the BrandsController as long as it updates a Brand object. A sweeper in Rails is a mix of Observer & controller filters, so all you need to do is “declare” it in the controller

class BrandsController < ApplicationController
caches_action :preview
cache_sweeper :brand_sweeper
def update
  ...
  @brand.save

Awesome sauce! Now our tests are red and I’m ready to implement the sweeper

class BrandSweeper < ActionController::Caching::Sweeper
  observe Brand # Observers will introspect on the class, but Sweepers don't

  def after_update(brand)
    expire_action :controller => "brand", :action => :preview, :brand_id => brand.to_param
  end
  ...

And voilà! We have greenness.

So what have we learned from this? The Rails source is still your best friend when exploring a sticky problem. Caching is hard, and testing caching is even harder.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Ken Mayer

SF Standup – Feb 13, 2012 – The Sound of One Hand

Ken Mayer
Monday, February 13, 2012

Cries for help

Capybara is reporting “stale elements” for no good reason.

Any experience, war stories upgrading from jQuery 1.6 to 1.7, especially with the whole event delegation API change? [crickets] See interestings…

Interestings

Have a really big spec file that takes a long time to run? http://rubygems.org/gems/parallel_split_test, from our own Michael Grosser, will run a big spec file in smaller chunks.

url helpers cache results for better performance, but they seem to be losing custom options (in this case a locales setting).

In jQuery 1.6, triggering an event in an unattached DOM node would propagate the event up into the document, even though it was never attached to it. jQuery 1.7 (maybe 1.7.1) fixes this behavior.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Glenn Jahnke

Performance stories – Writing two stories instead of one

Glenn Jahnke
Monday, July 25, 2011

TL;DR

Estimating performance stories are total bike-shed conversations.

  1. Write a figure-out-what-to-do story which generates the knowledge to estimate #2 (with repeatable benchmarks)
  2. Now estimate and execute the actual story around doing the optimization (and compare benchmarks)
  3. Repeat until your slowness is gone

Estimating Optimization is hard

Don’t Pre-optimize

We are always told to never pre-optimize our code. This is generally a good practice; it tends to keep you focused on writing readable, correct code. It also keeps you from making the common mistake of optimizing things that you think are expensive and are not anywhere close to the hot-spot.

Okay, okay, but something really needs it

Given enough time though, some part of the software you are writing will eventually become slow enough that it actually affects the usefulness of the code. A product manager should eventually see these performance-related issues and request stories around fixing it.

Avoid the Bike-Shed Conversation

Unfortunately, on many occasions I’ve sat at a planning meeting being asked to fix aforementioned performance issues, and I’ve nearly always seen a huge bike-shed conversation snowball, taking up lots of time, and rarely generating actual value such as what to optimize and how. Even worse is attempting to estimate the difficulty of optimizing. Optimizing code can have wildly different amounts of effort. Sometimes some simple cacheing can make huge impacts in a couple lines of code, but sometimes extravagant new algorithms are required which take weeks to write and test.

Estimating Two Stories instead of One

Optimizing might be hard, but avoiding wasting copious time estimating stories in Tracker is easy: write two stories.

The Find-Something-Juicy-and-Benchmark-It Story

The first story is simple: analyze your code. Try to instrument your code as best you can, it can be hard but make sure you get real numbers, and record your efforts. Find low hanging fruit in code that is both taking lots of time, and you have a couple ideas on how to make faster. The primary output of this story is your benchmark results, and an estimation of the work it will take to optimize. It may be useful to timebox your work, this will make your product manager happy because they know you won’t be running off into the distance chasing things that aren’t going to deliver value.

The Actually-Fix-It, Estimatable Story

The next story you write is simple, do the optimization, and then run the benchmark again to see that you got some results. Doing anything but going off of repeatable benchmarks is just silly. Having simple metrics which prove you actually sped things up is also very important as it is the only way a product manager can truly accept your story most of the time. Proving some batch process ran 30 percent faster is not fun for a product manager, letting them see a graph that proves it is.

Save Your Breath and Overestimates

Now your PM doesn’t have to choke on an hour long conversation, or a grossly over-estimated story because there is so much risk in actually delivering an optimization story. All you had to do was stop spending time trying to estimate things that are really hard to predict.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Ken Mayer

Standup 2010-11-16

Ken Mayer
Tuesday, November 16, 2010

Interesting Things

  • Project Sprouts from Luke Bayes “an open-source, cross-platform project generation and configuration tool for ActionScript 2, ActionScript 3, Adobe AIR Flash and Flex projects”

“Project Sprouts was originally designed (as AsProject) to solve a specific set of problems and was later entirely refactored to become a modular set of libraries that are built on top of Ruby, RubyGems and Rake.” — Luke Bayes

  • Sencha Touch 1.0 Ships – Now Free!

“So how will Sencha monetize? The company plans to sell its tools, like Sencha Animator, at a premium. It’ll also offer premium support plans.” — Tech Crunch

Helps

“Does anyone have experience with (slower) performance on EC2 compared to Heroku”

Some suggestions:

  • Test network lag
  • The small instance, the default, is just too wimpy to run as an application server
  • Make sure your database instance is big enough, and has enough memory
  • Any experience with RDS?

“Can you run cucumber with its own database instance?”

Responses:

  • By default, Cuke creates its own environment, but piggy-backs on the test database
...
cucumber:
  <<: *TEST
...
  • rake db:test:prepare is wired to :test and won’t support a :cucumber in the database.yml without extra rake tasks that have continuing maintenance costs
  • I’ve used the parallel_tests gem in the past, which has managed to retrofit db:test:prepare to work with multiple test databases, but it did require an extra step after each migration (and it didn’t play well with postgres text indexes).
  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Sam Coward

Identify memory abusing partials with GC stats

Sam Coward
Monday, September 13, 2010

Recently we had to investigate a page that had very poor performance. The project used newrelic, but neither that, the application’s logs nor render traces gave clear results. The database wasn’t the bottleneck, but there were well over 100 partials in the render trace. These partials seemed to perform well, except sometimes the same partial would take hundreds to thousands of milliseconds.

As it turns out, the cause of these random delays was actually garbage collection triggered by abusive memory consumption within many of the partials – millions of objects and heap growth in excess of 100mb. We discovered this by using the memory and GC statistics features of Ruby Enterprise Edition. You can get an idea of how much memory is being used and garbage collected during rendering by wrapping parts of your page in a block passed to this helper:

def gc_profile
  raise "Dude, you left GC profiling on!" unless Rails.env.development?
  allocated_objects_before = ObjectSpace.allocated_objects
  GC.enable_stats
  GC.clear_stats

  yield if block_given?

  growth = GC.growth
  collections = GC.collections
  time = GC.time
  mallocs = GC.num_allocations
  allocated_objects = ObjectSpace.allocated_objects - allocated_objects_before
  GC.disable_stats

  concat content_tag :span, "GC growth: #{growth}b, collections: #{collections}, time #{time / 1000000.0}sec, #{mallocs} mallocs, #{allocated_objects} objects created."
end

Because there were so many partials on the page, moving the helper around to identify the most egregious memory abusers got tiring so I wrote a monkeypatch to the venerable Rack::Bug plug-in which shows you memory consumption and garbage collection statistics for each template shown in the Rack::Bug template trace, as well as on the memory panel.

Example template trace

Memory panel example

First, install Rack::Bug and make sure it’s working properly, then add the code below in an initializer. Keep in mind this was written to work with Ruby Enterprise Edition 1.8.7 2010.02. Various 1.8 patches and Ruby 1.9 also provide some GC statistics reporting tools which you could probably customize this patch to use instead.

if Rails.env.development?
  require 'rack/bug'

  Rack::Bug::TemplatesPanel::Trace.class_eval do
    alias_method :old_start, :start
    def start(template_name)
      old_start(template_name)
      @initial_allocated_objects = ObjectSpace.allocated_objects
      @initial_allocated_size = GC.allocated_size
      @initial_num_allocations = GC.num_allocations
      @initial_gc_count = GC.collections
      @initial_gc_time = GC.time
    end

    alias_method :old_finished, :finished
    def finished(template_name)
      @current.allocated_objects = ObjectSpace.allocated_objects - @initial_allocated_objects
      @current.allocated_size = GC.allocated_size - @initial_allocated_size
      @current.num_allocations = GC.num_allocations - @initial_num_allocations
      @current.gc_count = GC.collections - @initial_gc_count
      @current.gc_time = (GC.time - @initial_gc_time)/1000.0
      old_finished(template_name)
    end
  end

  Rack::Bug::TemplatesPanel::Rendering.class_eval do
    attr_accessor :allocated_objects
    attr_accessor :allocated_size
    attr_accessor :num_allocations
    attr_accessor :gc_count
    attr_accessor :gc_time

    def memory_summary
      %{<strong>%.2fms</strong><small>in</small><strong>%d</strong><small>GCs</small>
      <strong>%d</strong><small>new objects</small>
      <strong>%d</strong><small>bytes in</small><strong>%d</strong><small>mallocs</small>} % [gc_time, gc_count, allocated_objects, allocated_size, num_allocations]
    end

    def html
      %{<li>
          <p>#{name} (#{time_summary}) [#{memory_summary}]</p>
          #{children_html}
        </li>}
    end
  end

  Rack::Bug::MemoryPanel.class_eval do
    alias_method :old_before, :before
    def before(env)
      old_before(env)
      GC.enable_stats
      GC.clear_stats
      @initial_allocated_objects = ObjectSpace.allocated_objects
      @initial_allocated_size = GC.allocated_size
      @initial_num_allocations = GC.num_allocations
    end

    alias_method :old_after, :after
    def after(env, status, headers, body)
      old_after(env, status, headers, body)
      @gc_count = GC.collections
      @gc_time = GC.time / 1000.0
      @allocated_objects = ObjectSpace.allocated_objects - @initial_allocated_objects
      @allocated_size = GC.allocated_size - @initial_allocated_size
      @num_allocations = GC.num_allocations - @initial_num_allocations
    end

    def heading
      "#{@memory_increase} KB &#916;, #{@total_memory} KB total, %.2fms in #{@gc_count} GCs" % @gc_time
    end

    def has_content?
      true
    end

    def name
      "memory_panel"
    end

    def content
      %{<style>#memory_panel dd { font-size: large; }</style>
        <h3>Garbage Collection Stats</h3>
        <dl>
          <dt>Garbage collection runs</dt>
          <dd>%d</dd>
          <dt>Time spent in GC</dt>
          <dd>%.2fms</dd>
          <dt>Objects created</dt>
          <dd>%d</dd>
          <dt>Bytes allocated</dt>
          <dd>%d</dd>
          <dt>Allocation calls</dt>
          <dd>%d</dd>
      </dl>} % [@gc_count, @gc_time, @allocated_objects, @allocated_size, @num_allocations]
    end
  end
end
  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Colin Shield

Marshal.dump vs YAML::dump

Colin Shield
Sunday, August 16, 2009

We find ourselves with a project with a very large dataset, more than 2 million items. This dataset changes frequently. The changes need to be transported to their respective servers ready to be served out to clients.
We decided to use a queuing architecture to distribute data. Objects are serialized and pushed to a queue. The large size of the dataset requires us to optimize as much as possible. There are only so many hours in a day and there is a lot of data to transport.
A question was raised in standup as to what was the fastest serialization method: YAML::dump or Marshal.dump. It seemed appropriate to write a quick script and work out which would be appropriate for our particular situation.
The objects we are serializing are simple hashes. I thought I’d write something that was representative of our situation in order to present a nice clear decision.
Here’s some code:

require 'yaml'
obj = {:a => "hello", :b => "goodbye", :c => "new string", :d => {:da => 1, :db => 2}, :e => 1}
start = Time.now
(0..10000).each do
  ser_obj = YAML::dump(obj)
  new_obj = YAML::load(ser_obj)
end
puts "YAML::dump time"
puts Time.now - start
start = Time.now
(0..10000).each do
  ser_obj = Marshal.dump(obj)
  new_obj = Marshal.load(ser_obj)
end
puts "Marshal.dump time"
p Time.now - start

I think we all knew how the results would look. It was nice to see that for our particular case there was a clear winner.

YAML::dump time
5.397909
Marshal.dump time
0.280292

Seems fairly cut and dried to me.
I personally prefer YAML for test result comparison. Maybe we’ll put something in our spec_helper to use YAML for testing and Marshal for production.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Joseph Palermo

Careful when doing a :include with a :has_one :through association

Joseph Palermo
Monday, January 12, 2009

We had an odd bug last week where we ended up with different results after we had eager loaded an association vs loaded directly.

There are apparently two issues with :has_one :through, one of which also applies to :has_many :through.

So given:

class Person
  has_many :friendships
  has_one :best_friend, :through => :friendships, :conditions => "friendships.best = 1"
end

If you do a Person.find(:all, :include => :best_friend), the best_friend that gets preloaded is not necessarily one that has a “friendship.best = 1″

This is due to a bug in the association preloading code that doesn’t pass down the finder options, so any :conditions or :order are completely ignored. This problem is easy to fix, just a one line change, but it then exposes another problem.

This problem applies to both :has_many :through and :has_one :through associations. The problem is that the :through association is loaded separately from the :has_one or :has_many association. So it first loads :friendships, and then when it tries to load :best_friend, it doesn’t have the table it needs for the :conditions and explodes.

Our current work around is basically putting the conditions on the :through association, although sometimes you need to create a new association just for that which is certainly not idea, especially if you plan on accessing the :through model after it has been loaded.

The way to fix it in Rails is unfortunately a rewrite of how the :through associations are eager loaded.

You can see the lighthouse ticket here

There is also a couple of messages on the Rails Core group

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Pivotal Labs

New York Standup 10/1/2008

Pivotal Labs
Wednesday, October 1, 2008
  • There is a ruby meetup tonight at Outside In, at 7pm.

  • For load testing, Load Testing With Log Replay from igvita.com has a review of several common tools.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter

Topics

  • agile (780)
  • rails (113)
  • testing (88)
  • ruby (83)
  • ruby on rails (70)
  • jobs (62)
  • javascript (55)
  • techtalk (44)
  • rspec (38)
  • ironblogger (32)
  • productivity (30)
  • activerecord (29)
  • gogaruco (29)
  • git (28)
  • nyc (27)
  • rubymine (26)
  • bloggerdome (23)
  • mobile (22)
  • process (21)
  • pivotal tracker (20)
  • cucumber (20)
  • jasmine (19)
  • design (18)
  • ios (18)
  • webos (17)
  • objective-c (17)
  • android (16)
  • palm (16)
  • "soft" ware (16)
  • fun (15)
  • tracker ecosystem (15)
  • ci (15)
  • cedar (15)
  • rails3 (14)
  • performance (14)
  • bdd (14)
  • gem (13)
  • css (13)
  • tdd (13)
  • selenium (12)
  • goruco (12)
  • bundler (12)
  • meetup (11)
  • railsconf (11)
  • nyc-standup (11)
  • capybara (10)
  • mac (10)
  • mojo (10)
  • chef (10)
  • api (10)
Subscribe to performance Feed
  1. 1
  2. 2
  3. →
  • About
  • Case Studies
  • Team
  • Community
  • Careers
  • Contact
  • Labs
  • Events

Contact Us

contact@pivotallabs.com
+1 415-77-PIVOT
TwitterLinkedInFacebook

Pivotal Tracker

Tracker is the award-winning agile project management tool that enables real-time collaboration around a shared, prioritized backlog.
Visit pivotaltracker.com >