quick aside ➡ how many of you write ruby almost every day? ➡ how many of you write ruby that's deployed to a web server? Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 4
CocoaPods combines a definition of libraries (podspecs) with a way to integrate them into a user's application (podfile) Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 7
Xcode ➡ Proprietary toolchain for compiling apps for apple platforms ➡ Uses its own manifest file format ➡ Invokes many compilation tools Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 10
Why Performance? ➡ Optimization is fun! ➡ Rapid iteration == happier & more productive developers ➡ "Scale" ➡ Free performance is hard to find Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 15
How do I make it faster? ➡ Is it really slow? ➡What is it doing? ➡How often does it run? ➡Would it being faster make a difference? ➡ Can it do less work? ➡ How can I keep it from getting worse? Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 17
Is it really slow? ➡ How much would I invest to make it 10% faster? 50%? 90%? ➡ How do I know which part of what it's doing is slow? Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 18
➡ ! this feels slow ➡ ⏱ this took 10 seconds ➡ # this segment took 10 seconds ➡ $ this method was called 97645 times, averaging 23ms per call, taking up 48% of total runtime Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 19
quick aside: One of our contributors got amazing gains by profiling allocations and memoizing things like #hash & Pathname#join Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 24
Profilers Just like most tools, it's important to pick the right one for the job at hand Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 25
Tracing Profilers Tell you how many times something was called, and usually how much time is spent in that call. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 26
Tracing Profilers ➡ Can distort relative numbers ➡ Can make running a program painfully slow Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 27
Tracing Profilers If it takes 35 minutes with no profiling, it may never finish under rubyprof. Sorry, I didn't think of that. — CocoaPods/CocoaPods#5180 Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 28
Allocation Profilers Tracing profilers that count memory allocations instead of method calls. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 29
Allocation Profilers ➡ Suffer from similiar problems, but even more so since the typical ruby program is incredibly allocation-heavy. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 30
Sampling Profilers Takes a peek at your program at regular intervals, from the outside. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 31
Sampling Profilers ➡ (Generally) will not interfere at all with your app's normal execution. ➡ Possible to miss things, it's luck whether a particular stack trace gets sampled at the right time. ➡ Can’t tell you how many times something has been called. ➡ “Here were the stack traces when I looked” ➡What was at the top of the stack at the time? Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 32
Manual Profilers Sometimes, you don't want or need a fancy tool. The easiest output to understand is the one you write yourself. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 33
Manual Profilers ➡ Wrapping method calls in a call to Benchmark.measure will tell you how long it took. ➡ Can help you focus in on what you already suspect to be hotspots ➡ More likely to tell you if the distribution of calls is not unimodal ➡e.g. calling a memoized method, so only the first call will take a significant amount of time Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 34
Chronometer ➡ Wraps methods with some timing / recording infrastructure ➡ Can output results in a format compatible with chrome://tracing ➡ Happens in-process, so you can write code that uses the timing results Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 36
Profilers For me, this meant a mix of ➡ rubyprof ➡Tracing Profiler ➡ rbspy ➡Sampling Profiler ➡ chronometer Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 38
All 3 are amazing, and I used all of them extensively last year. But knowing which to reach for at each step of the investigation would’ve saved me a bunch of time Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 39
Chronometer hit the sweet spot for the type of software I was developing, and also solved a couple other problems I had. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 40
Graph Traversal The base problem we often face is How do you find “all the nodes/vertices that come after A”? Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 48
It's tempting to say: “A is followed by B and C, let’s recurse!” But then you end up visiting D twice! This might not sound like a big deal, but imagine 50 more nodes come after D... Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 50
DAGs ➡ Directed Acyclic Graphs ➡describe dependency structure. ➡ Every node (target, library, gem, etc.) has a list of dependencies ➡only rule is “you can’t depend upon something that depends on you” ➡ Most operations rely on what’s called the “transitive closure” of a node ➡“what are all the nodes that come after this one, no matter how far away?” Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 53
def recursive_predecessors vertices = predecessors vertices += vertices.flat_map(&:recursive_predecessors) vertices.uniq! end to def recursive_predecessors vertices = Set.new visit = ->(vertex) do vertex.incoming_edges.each do |edge| vertex = edge.origin next unless vertices.add?(vertex) visit[vertex] end visit[self] vertices end Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 54
Something that comes along with trying to solve graph traversal problems is memoization! Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 56
➡ This is slightly different from caching ➡There’s no “invalidation” that can happen ➡ We’re just lazily computing a value, and storing it so next time it’s needed, we can return the stored value Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 61
In ruby, we do this a lot! ever seen something like this: def expensive_to_calculate @expensive_to_calculate ||= ... end Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 62
This works great! ➡ But imagine if the return value for expensive_to_calculate can be nil, or false. ➡ The ||= short-circuiting won’t kick in. ➡ We’ll need to recompute the value every time. ➡ Not ideal. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 63
Instead, we need to turn to a more complicated variation on the pattern: def expensive_to_calculate return @expensive_to_calculate if defined?(@expensive_to_calculate) @expensive_to_calculate = ... end Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 64
In CocoaPods, when I re-wrote the build settings, I added a tiny little DSL to make memoization a bit easier: def self.define_build_settings_method( method_name, build_setting: false, memoized: false, sorted: false, uniqued: false, compacted: false, frozen: true, from_search_paths_aggregate_targets: false, from_pod_targets_to_link: false, &implementation) Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 65
➡ It uses an ivar hash called @__memoized to store those values, calls #fetch on that hash, returns the value unless the hash doesnt yet contain the key. ➡ One benefit of this is that the memoized state can be easily discarded by clearing that one ivar, instead of needing to keep track of a bunch of ivars, each only used in a single method implementation. ➡ By carefully selecting the key, we’re able to memoize both what a superclass and subclass return separately, so multiple calls to super can also be memoized. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 66
This DSL (and accompanying refactor) allowed me to fix years-old bugs in build settings generation. Bug fixes which would've slowed installation down by over 5 minutes under the old implementation. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 67
Design Lessons Learned Even after all this work (and it’s still ongoing!), CocoaPods isn't as fast as I’d like. And it never will be. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 69
➡ Some of this comes down to language. ➡ Some of this comes down to the fact that the job CocoaPods does is inherently complicated. ➡ Some of it is our own fault, for having a system design from 7 years ago that wasn’t built to scale this far. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 70
Exposing Mutable Objects ➡ Makes memoization hard. ➡ Need to have sidecar objects that hold computed values only when its safe to do so. ➡ (unless you want to get into cache invalidation bugs) Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 72
Having Consistent CLI Output ➡ Once again, makes parallelization hard. ➡ How do you show: ➡progress ➡underlying command invocations ➡failures Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 74
Inefficient Data Structures ➡ Particularly the podfile & podspec. ➡ The entire Xcode project model ➡ Built using nested hashes, making change-tracking hard. ➡ Lack of copy-on-write. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 75
Using Ruby to eval Objects Stored on Disk ➡ Allows state to leak in, makes computing stable hashes complicated because there are multiple valid representations to compute them from. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 76
Not Storing all Podfile information in the Podfile.lock ➡ Figuring out when its changed based on only the info checked into git is nearly impossible. Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 77
All-or-Nothing Installation Operation ➡ Needing to do the entire installation every time means all invocations are equally slow. ➡ Fixed now, thanks to @sebastianv1! Making CocoaPods Fast – Samuel Giddins @ RubyConf Taiwan 2019 78