Ask HN: How do you familiarize yourself with a new codebase?

407 points by roflc0ptic ↗ HN
My question is pretty straightforward: how do you, hacker news enthusiast, familiarize yourself with a new codebase? Obviously your answer is going to be contingent on the kind of work that you do.

Some background: What's motivating me to ask is that I am flirting with the idea of trying to add a couple of features to SlickGrid (https://github.com/mleibman/SlickGrid), Michael Leibman's phenomenal javascript grid widget. Unfortunately Leibman got busy and isn't actively supporting it anymore.

The codebase is something like 8k lines of javascript, so it's not ludicrously big, but I'm kind of intimidated thinking about trying to make sense of it. My first strategy is just to open up important-looking javascript files (slick.core.js, slick.grid.js) and read through for comprehension. This seems like a pretty slow way to build a mental model of the code, though. Some features I want to implement are 1. an ajax data source that doesn't require paging, and 2. frozen columns. Someone else has implemented a buggy version of frozen columns (and since abandoned the project), and I might like to use it, but I can't tell if it's buggy because it's a hard problem, or because their implementation strategy was poor (or both!). So at the moment I can't evaluate if I should implement my own, or try to fix the issues with theirs.

Picking up other people's code seems to be one of the harder tasks developers face, as evidenced by how much code gets abandoned, so I wondered if the voices of experience on here could point me in the right direction, either by talking about this problem in particular, or more generally, how you build knowledge about a new codebase.

Thanks!

245 comments

[ 7.6 ms ] story [ 177 ms ] thread
I personally like to reverse engineer functions within a certain codebase to better understand what is happening.

For example I would start by looking up out a basic example of that codebase and for each of the function calls go through the files and see what is happening. This gives me an idea of how the code base is written and how it works. It also gives a clear understanding of the level of separation/specificity of the different functions.

Disclaimer: not very experienced so there might be better ways to familiarising ones self with a new codebase, this is just one way of doing it and it has worked for me in the past.

(comment deleted)
Drawings will help tremendously. Extract the big masses, their respective interfaces to each others and the means through which they communicate. This will help build a mental map of the code and reduce the cognitive load needed to understand each separate part.
I generally just start by fixing small bugs in different areas of the system. I find that debugging various areas of the system help me understand them better and allow me to start forming a cohesive picture in my mind.
I wish I had a better answer, but I honestly just stumble around it. I typically start by trying to understand how they structured their files, then I'll start diving into the code. I wouldn't try to "understand" it completely. Just look over it until you feel comfortable enough to try to make some modifications.

Michael's code looks clean and well organized. Shouldn't be terribly difficult for someone proficient at JS.

Glad I'm not the only one who stumbles around :)

Another thing I do is try and replicate one core feature of the thing I'm trying to understand. Like others have suggested, I like using debuggers. Recently, I wondered how debuggers work. So I built the following to find out.

https://github.com/amorphid/rails-debugger_example

This may or may not apply to you, since i work with Perl. Typically i'm in a situation where i'm supposed to improve on code written by developers with less time under their belt.

As such my first steps are:

1. tidy/beautify all the code in accordance with a common standard

2. read though all of it, while making the code more clear (split up if/elsif/else christmas trees, make functions smaller, replace for loops with list processing)

While doing that i add todo comments, which usually come with questions like "what the fuck is this?" and make myself tickets with future tasks to do to clean up the codebase.

By the end of it i've looked at everything once, got a whole bunch of stuff to do, and have at least a rough understanding of what it does.

Please don't take this as a criticism, but how long have you been programming? I'm asking because I used to have an opinion like this when I was just starting, but after a few years I realized that changing all of the code as the first thing is one of the worst things to do.
>> i work with Perl >> user: Mithaldu >> created: 2099 days ago

> how long have you been programming?

For pay since 2005. Do keep in mind that the code i am working with usually has some sort of test suite available, and that over time i have become very good at transforming code between different forms of expression without changing the effects it causes. (Excluding memory use and performance, which is not something one usually has to consider much in Perl.)
Ah, so long enough for the advice to be based on real experience.

I was really surprised to see it, because it's exactly the way I was learning C. I switched to Linux around the same time, so I'd take some abandoned DOS program that had source code available and port it to Linux. During the process, I'd read the entire source code, make sure I understood it and reformatted everything. A few years later, the original author of one of the programs released a new version and thanks to my reformatting, it was pretty much impossible to merge. After a few similar experiences at work, I have decided to always stick with the original style of any code I touch, because any unnecessary changes are just going to make life harder for me in the future.

Three things to keep in mind here:

1. Perl is MUCH more concise than C, since we have institutionalized code sharing (see CPAN), whereas most C devs i know (and maybe i don't know too good ones) seem to at most reuse code others wrote by way of ctrl+c/ctrl+v.

2. Perl's reformatting tools are automatic. I just have a little config file that says how long my lines are supposed to be and where i'd like the spaces on my parens (inside, between the parens and arguments) and then i hit ctrl+e and boom it's done. If i need to do it to many files, find + perltidy. In your case i would've just taken his new version, automatically formatted everything in less than 5 minutes, and merged on top of that.

3. When i do this, i'm doing it with team lead consent and team concensus, in an authority position, not with random code maintained by people i never even talked to. :)

The big problem (apart from the fact that ill-formatted codebases have often much more serious problems...) is that reformatting is an excellent way of messing up your VCS' diff ability, which is extremely precious in trying to understand why things have been done the way they have.
Both points aren't possible to do with large codebases.

It will take months merely to tidy the code with the effect of making the rest of the team hate you for committing thousands of files for superficial changes. Its much more productive for everyone to simply adapt to the existing style guidelines.

Reading all of the code is only an option for the smallest of codebases. Reading code will only get you so far before you get lost in the complexity of how all the parts interact with each other.

A better approach would be to limit yourself to a subset of the codebase and start poking around with a debugger while the system is running. Then you can gradually work your way through the codebase starting from the core functionality.

If i take longer than a week to do it, then other measures are called for, like, as you mention, subsets. However you seem to underestimate just how much functionality can be implemented in how little code with Perl.
Oh I wasn't talking about Perl specifically but about languages in general. I agree that Perl projects tend to stay small enough to be quickly understood and refactored. In fact I don't remember seeing a Perl script more than a few thousands line long.

However, don't underestimate how hard it can be to understand your own Perl code when you've been away from it for 6 months :)

> However, don't underestimate how hard it can be to understand your own Perl code when you've been away from it for 6 months :)

For anyone who cares about their craft and has a certain amount of experience with Perl this is entirely untrue. I'd rather people not repeat this tired old meme. :(

Thank you! :)

vvvv

Sorry, you're absolutely right, I've seen modern perl and it was actually quite clean. I didn't mean to offend by saying this!
Do you follow this process even on code without tests? How do you make sure you're not introducing bugs in the process? Or you don't commit your changes afterwards?
It works even on code without tests, simply due to lots of experience. It helps that i also have a lot of tools available in terms of realtime syntax and sanity checkers. And sometimes if the code is sufficiently insane enough, i'll write tests to make sure.
A while back I worked with team that had just brought on some new developers. Some of the developers were eager and would learn with out human intervention. Others would require and ask for some mentoring.

Neither way of learning is right or wrong and I appreciated both groups but one thing I did remember was one guy that did exactly what you did and it was highly irritating as a project lead to see relatively random files (given whatever sprint we were on) that were considered stable to show up in the source control change list with out some consideration of discussion. I would have to waste time and look what change he made to see if it was ok.

Understandable. I'm also operating quite differently. Typically i am called into teams who are floundering already. So what i do is create one big commit with the goal of making the code easier to work with and to learn about the code base. Never do i just change random files, and never do i do it without the team lead having agreed and some sort of concensus from the rest of the team.
Well, that doesn't scale at all. I don't remember the last time I've worked on a project where even skimming all of the code would be possible in a reasonable amount of time, much less actually reading it and refactoring it.

That said, it probably does scale to the OP's 8k line code base.

Also, running the code through a formatter and refactoring it all right off the bat is a sure fire way to piss off everybody else working on the project.

In any case, my 2 cents for the OP is to not bother trying to learn the whole codebase, but instead focus on just the areas you want to enhance. For example, in the case of the new data source, find out how the existing data sources are implemented, and use them as examples for adding a new one.

read tests, and then start writing tests for things.

something usually comes up.

I usually skim the code to get an idea of patterns and organization, get it working in a local environment and then run/step-through the code. This usually gives a good idea of what different pieces do.
You got a little bit lucky with this project because there's a decently built-out test suite. I would start by digesting the tests because if they're good, you'll be able to see the mechanics about how the exposed interfaces in the code work, and this should also give you a good idea if changes you're making are breaking the expected workflow or not.

From my experience, there are really two ways that learning a new codebase can happen. One is that there's an existing test suite that's fairly comprehensive, and you can learn a lot by examining the tests, making changes to add features / make bug fixes, and then validate that work by rerunning the tests and adding new ones. That's really a great place to be as someone unfamiliar with a new codebase. The other is that there are no tests, and you inevitably need to rely on people familiar with the code, and make peace with the idea that you're going to write bad code that breaks things as you learn the depth of how the project works.

I've written scripts to read files and match function calls to their definition/body and output text "trees"; but the process deserves some better visualization, navigation of dependency graph/comprehension specific highlighting. I'd be interested in trying an IDE that can do this.
Debugger! Surprised no one has mentioned it yet. I work in js and php, both of which I use the debugger a lot.

Set a breakpoint, burn through the code. Chrome has some really nice features - you can tell it to skip over files (like jQuery) you can open the console and poke around, set variables to see what happens.

Stepping though the code line by line for a few hours will soon show you the basics.

What debugger do you use for PHP? I've yet to find one I really like.
xdebug and phpstorm is great.
Xdebug is de facto the only debugger AFAIK. The integration in Phpstorm is great.
http://codebugapp.com is a fantastic front-end for Xdebug. I get a lot of mileage out of this, not least because I only had to set it up once and now it works with whichever editor I'm trying out this week.
Xdebug and Netbeans, tons of tutorials on how to set it up on google/youtube.
NuSphere is better than xdebug. It allows more actions and is generally better.

But I use xdebug and phpstorm. I find it better than netbeans. Search in netbeans is good, but the whole thing has a weird GUI and is slow.

Best bet is to try a bunch.

Xdebug and with phpstorm 9 you have inline display of the values held in that variable which you can expand with a click.

That is incredibly powerful (PyCharm has it as well).

Codebug. Phenomenal Xdebug client.
I am surprised how few younger programmers use a debugger these days.
I'm not a younger programmer and I don't use a debugger. Tried it several times and found it counterproductive.
Like with most useful tools, you have to use it more than "several times" to gain fluency.
Indeed, and learning to use a debugger will allow you to gain fluency in new languages and frameworks much faster that the simple, albeit powerful, printf.

I remember needing to learn Ruby and Rails on an existing closed codebase and having the debugger take me right to the core of Rails several times to understand why certain things were done in a certain way in the top level code. This allowed me to get acquainted with internals of Rails and how Ruby's inheritance system much faster as well as the existing codebase that was built on top.

How is it counterproductive exactly? The setup of the debugger itself? Or trying to figure out how it works?
It's a complicated explanation I suppose, but I would say that among other things, I've seen my colleagues abusing debugging and since debugger is a "single threaded" and sequential approach if you will, they were losing the big picture. I try to understand the code and keep it in my memory so that I could predict the behaviour without the debugger. On a better day this would be a better explanation.
> I try to understand the code and keep it in my memory so that I could predict the behaviour without the debugger.

That's the theme I hear when prodding people who (sometimes loudly) say they don't use debuggers. They work on codebases that are either small, solo projects, or change very slowly. That way they can keep an accurate simulation of the entire program in their heads.

Meanwhile, I've always worked on codebases that were changing faster than I can keep up. The majority of my debugger time has been in code I've never seen before.

Not necessarily. For instance, my colleagues work with a fairly large codebase in java which runs on top of a framework written in scala. When they tried to hook up a debugger it drilled the whole stack top to the bottom, naturally and showed them whole lot of scala code. Problem is, these guys don't now scala, only java.
My debugger shows a full stacktrace too. I usually ignore the Django code that's its written on top of and use it in the sections I have written.

I don't find it a hindrance in any way seeing what part of the Django code called my code.

The debugger being sequential is a consequence of your code also being sequential at the CPU level.

Understanding the code is often a luxury we don't have, either because we didn't write it in the first place or because there are just too many moving parts each tracking their own state and interacting with everything under the sun.

This is especially true for long-running programs that do much more than just transform inputs into outputs. For example a video game would be complete hell to develop without a solid debugger. Even if you believe you understand the complete code and all its behavior the testers will always find a way to make the game crash after 2 hours of playing leaving the game's state completely broken and without a debugger you'll be scratching your head endlessly trying to replay how that happened.

In these cases the number of different states and behaviors you can get out of the system is incredibly high, easily a few orders of magnitude more than the human brain can handle.

> or because there are just too many moving parts each tracking their own state and interacting with everything under the sun.

For me at least, that's typically my cue to start refactoring. If I can't model at least the general behavior of my program in my head, then it's probably needlessly complicated.

This isn't always possible, of course (such as in the case of long-running programs, as you mentioned; it's still possible in a lot of cases, though, and splitting off functionality into smaller, easier-to-digest pieces can make troubleshooting much easier), but it's been a useful mentality for me, and has significantly reduced the need for me to use some sort of debugger or even 'print' statements to make sense of code.

This doesn't address the original question of understanding new codebases, though.

I'm not sure how you "abuse" debugging-- can you elaborate on that?

The debugger runs in as many threads as the app itself. If you're debugging C#, and your program is in 37 threads, you can debug all of them simultaneously or debug one and ignore the others.

Understanding the code and keeping it in memory sounds like a great practice, but it also seems entirely orthogonal to using a debugger.

I have the exact opposite attitude: I live in the debugger. Most of my resistance to trying "new hot" languages is their universally terrible debuggers. (Usually they either don't exist at all, or only exist in CLI form.) IMO, if you don't have a working, stable, graphical debugger, your programming language has no business being 1.x.

It depends on what you're working on.

If you're working on, say, a huge Java codebase, then a debugger is practically essential because you've got a lot of code to navigate, and you probably want to see what flavour of objects are being passed around and how their state is being updated.

On the other hand, if you're working on, ooh, a Nodejs codebase, you're probably looking at less code, with a debugger that's much slower, and probably more functional code that is operating on data directly. Using a debugger in that case is often slower than just using print statements.

I'd argue that print statements are a debugger, albeit a custom one off debugger. Sounds like the actual debugger needs work (... or from my experience it's me who doesn't understand the debugger correctly)
That argument is a bit of a stretch. Just because you are debugging something doesn't mean you're using a debugger...
I just use printf and other stuff to dump critical variable. I also use unit test a lot.
Ah yes, the good ol' printf-debug-polluting-codebase method. What's fun after that, is when programmers all start competing to have their printf's be more visible in the sea of prints ... I find this method primitive at best. How do you manage it?
You remove the printfs when you have fixed the problem.
It's so tempting to do this. Don't. Problems may occur again in the same area. Leave your logging statements around; configure your logging setup to skip them when you don't want them.

You're saying to delete commented-out code once the new code works. Put your code in version control instead. Logging is to once-off printfs as version control is to commented-out code.

Not so sure. Once I get to the stage where I have to stick printfs in the problem tends to be so localized and specific that once fixed I will never need to look at that area of code in that way again.
I do this far more than I should. The code ends up a mess after enough debugging like that.
I think he meant to add print statements locally. You remove them before committing the code.
I find this method primitive at best. How do you manage it?

Don’t be primitive about it. :-)

The serious answer is: use a good logging framework. In particular, I’d suggest something that can log at selective levels of information for different parts of your system.

If your project’s culture is to instrument your code like this routinely, it can be a very useful asset, and it’s no more disruptive in practice than say adding comments or writing tests. In each case, with experience you get better at judging where to focus your efforts and how much detail is worth including by default, and if that turns out not to be enough you can always add more while you’re working in that area.

With modern tools for recording and analysing logs, there is relatively little useful information that you can easily find using a debugger but can’t easily find from good log output. On the other hand, logging has some big advantages: you can capture how your system changes over time, you can record and review concurrent behaviour cleanly, you can capture information from different parts of distributed systems that might be written in different programming languages or running on different devices.

In my case, aggressively reject any printf statements in code review. Get that shit out of my repo.
Without trace statements, how do you diagnose issues that don't occur locally and only appear in the production environment.
That would be the difference between printing and logging
Traces are the kind of thing that should be automated anyway... You shouldn't have to write explicit trace statements in your source code, instead your language or framework should have an automated way of inserting these statements at function boundaries. Or even better, attach a debugger and reproduce the issue yourself on the prod server.

Just plain "debug log statements" I have yet to hear a good use case for. Every time people have put them in, it's because some bug called for their inclusion, then the bug got fixed, then the statements got left around after the bug was fixed. Or the bug didn't get fixed and it's a matter of "why don't you fix the bug?"

There's rare cases where there's an ongoing issue with a known bug that you don't know the fix for yet, so you drop debug statements in production code hoping to catch it. But this is supposed to be a rare, rare case.

I tend to do that mostly for concurrent programs, as I want to see what happens without blocking the entire system like a debugger would.

For medium/large programs I'll often prefer a debugger because I can easily and quickly try to diagnose the problem without having to wait for a new build to complete with the added traces.

printf = breakpoint expression evaluation (but without the flexibility)

You're using a primitive debugger, you just don't know it.

It does have the advantage of both immediacy and continuation.

You can dump statements through a function and see it's progression when run without having to interact with it.

Throw in something like FirePHP (which allows dumping pretty much anything out as a viewable/collapsible trace, other languages have similiar) and the use case for a lot of using a full blown debugger is removed (it's still incredibly powerful when needed).

So I use both.

Codebug + Xdebug, setting a breakpoint and getting a full REPL at that execution point is far more useful to me when I'm in PHP land :)
Debuggers and profilers are essential tools, but I was surprised when I read the book "Coders at Work: Reflections on the Craft of Programming" that a lot of the greatest programmers debugged just with a printf.
Maybe this is just my lack of experience speaking, but I find debuggers incredibly difficult to use when dealing with interactive software (which is most software I work on). Hitting a breakpoint freezes my interaction unless I configure it to print something, and configuring it to break conditionally requires knowing some obscure incantations that I can never remember off the top of my head (and that in turn require their own debugging). So it boils down to: should I spend 5 minutes figuring out how to set this conditional, print-only breakpoint? Or should I just put in a printf? The solution, at least to me, is obvious.

(With that said, when I have a nasty bug that requires squishing around in the guts of my program, breakpoints are invaluable. But it's a pretty huge hammer that only comes out on occasion.)

EDIT: I wonder how useful an editor that allows inline debugging code would be? Instead of setting breakpoints in your IDE and then configuring them in an individual window, you'd enter a "debugging editor" mode that would allow you to add debugger-only code for things like conditional breakpoints and printf statements right alongside your normal code. The original source files would not be edited, but while in this mode, it would appear as if they were. (Maybe the debugging code would show up in red.) That way, you could easily access all your local state and implement complicated queries without ever leaving the context of your code. In other words, it would be just like printf debugging, but once you no longer need to debug you'd just collapse those statements out by leaving the debugging editor mode and your original code would be unchanged. Perhaps this debugging code would not compile along with the rest of the code but would instead use the debugging hooks that normal breakpoints use, unless the conditions are particularly complex or something. Just a thought.

Not sure if you know this but if you're in javascript you can just drop a 'debugger;' statement anywhere in your code and it will function as a breakpoint.
Ah yes, I should have mentioned that this is coming from a native application developer!
Debugger++

Without a debugger you're a sitting duck!

Debugging through the test cases in particular is a good way to decipher/dissect things, at least that I have found. Usually you can find a test case that is only for a specific component that you are interested in, and then the test case should only exercise those pieces, so there is not an overwhelming amount of information all at once.
When I have a new code base that I'm unfamiliar with and need to understand it quickly, I'll go line-by-line and add comments about what I believe to be the intended behavior. As I gain more knowledge I'll update the comments. For me explaining something I've learned helps me commit it to memory better, and makes sure I really did comprehended what I just read.
Well, I'm not very good at this either but here's what I do. I usually work on modular projects where there are hundreds of files in the project. I usually skip directly to locating the file where I've to make amends (using a lot of grep. grep for function and object definitions, grep for usage patterns, grep for checking how to implement something). Thus, I learn about the codebase as I go along.

Sure, this is not the best practice, and unsuitable for many, but it's what works for me.

If this is your modus operandi your life will be much improved with Ack. It's specifically designed for searching codebases.
Amen. Once you've had ack, you never go back.
Isn't ag a better option these days?
Thanks. I'm looking at it right now, and it looks really cool.
I think you should also check out cscope, it can search for struct definitions and function calls easily (better than just grepping text or using ctags).
This is usually how I do it for libraries:

* Read the README.

* Install it and start using it with a couple of sample cases. That will give you an idea of what it does.

* Read the test suite. This will give you a better idea of what the library does.

* Look at the directory structure. This should tell you where things are.

* Start reading the core files.

* Start looking at open issues. Try to solve one by adding a test and changing the code.

* Submit a pull request.

I agree with what many others on here have said. It's also a personal thing. In general I like to try to force myself to learn only the minimum required to do what I need to do. If that philosophy sounds good to you, I would recommend taking the buggy version of frozen columns and try to fix the bugs. You may learn that the bugs are structural and you need to implement it differently, or you might be able to fix it with minimal changes. You will certainly get an understanding of the parts of slickgrid that you need to interact with to add this feature.

For the ajax data source thing, I would try to modify or extend the existing data source code to add the behavior you are looking for. As you mess around with it trying to figure out what you need to change, you will encounter the areas of the code that you need to understand.

With this sort of strategy you can avoid having to fully understand all the code while still being able to modify it. You might end up implementing stuff in a way which is not the best, but you will probably be able to implement it faster. It's the classic technical debt dilemma: understanding the complete codebase will allow you to design features that fit in better and are easier to maintain and enhance, but it will take a lot longer than just hacking something together that works.

My approach is to break stuff. If I can break it (and I am good at finding bugs, so I usually can) then I now have a narrow focus which helps me getting "lost" in the code base.

Once I've found and fixed a few things, or if the code base is particularly small or clean that I can't find bugs to fix, I'll set about hacking in the feature I'd like.

I usually start by doing it in the most hacky way possible. That sounds like a bad approach but it narrows the search of how to implement it and means I'm not constraining myself to fit the code base that I don't yet appreciate.

In hacking that feature I'll often break a few things through my carelessness. In then trying to alter my hacked approach so it no longer breaks stuff I'll become more aware of the wider code base from the point of view of my initial narrow focus. This lets me build up the mental model.

Eventually I'll be comfortable enough I can re-write the feature in a way more consistent with the wider code base.

I don't normally start by trying to "read all the code" because that guarentees I won't understand much of it (I'm not quick at picking up function from code). I might have a skim if it is well organised, but I find the "better" written a lot of stuff is, the harder it is to grok what it is actually doing from reading it. to me, reading good code is often like trying to read the FizzBuzz Enterprise Edition[1].

I've worked on many legacy systems: I was last year implementing new features into a VB6 code base, this year (at a different job) I am helping migrate from asp webforms to a more modern system. I've found that starting with trying to fix an issue to be the best way to dive into the code base.

Use good source control so you're never "worried" about changing anything or worrying that you might lose your current state. Commit early, commit often, even when "playing around".

[1] https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...

First, have in your mind what the function of the chunk of code is. If it's not important to the system, skip it, don't read it. If it is important to the system, take a guess how you think it should work, how you would probably implement it if you were the original develop. Then begin reading it.
Build it, if there is something to build. Scripting languages usually don't have builds but JS minification and dependencies installation could be a build. Find and read the code paths that perform some recognizable action. Run tests, read them. Add a new feature with tests or pick an open issue and fix it. You're going to have to debug something and that will give you more insight in the inner workings of the code.
When you think you understand something write a test and test your belief. If the test passes then both your knowledge and the code base are better for it. If the test fails then rewrite the test to the failure and write another test. Again you will know more and the code base will be better.

Good luck.

I feel your comment should be the top one. but I disagree with this bit:

> the code base will be better.

I'd change "will be" to "might get." because this is true if you're doing unit tests that the code base can use. but sometimes you do characterization tests, which are not worth keeping around. or you might build a couple variations on "hello world" with the unfamiliar code base, just to be sure that it works the way you think it does.

When writing a test exacerbates a too-many-tests problem, it's both a rare and and good problem to have because reading and running such tests is a more expeditious route to understanding than writing tests in the blind.
What if you wrote a test that passes at the time of writing because of how something is implemented at that time but its not actually an invariant?
Then you will learn that later when the test fails. That is better because the reason for writing the test was your belief that it was invariant. Without the test you are more likely to continue holding the mistaken belief.
Sure, but that is an incorrect test for other developers to worry about it failing.
A post from last year, "Strategies to quickly become productive in an unfamiliar codebase": https://news.ycombinator.com/item?id=8263402

My comment from that thread:

I do the deep-dive.

I start with a relatively high level interface point, such as an important function in a public API. Such functions and methods tend to accomplish easily understandable things. And by "important" I mean something that is fundamental to what the system accomplishes.

Then you dive.

Your goal is to have a decent understanding of how this fundamental thing is accomplished. You start at the public facing function, then find the actual implementation of that function, and start reading code. If things make sense, you keep going. If you can't make sense of it, then you will probably need to start diving into related APIs and - most importantly - data structures.

This process will tend to have a point where you have dozens of files open, which have non-trivial relationships with each other, and they are a variety of interfaces and data structures. That's okay. You're just trying to get a feel for all of it; you're not necessarily going for total, complete understanding.

What you're going for is that Aha! moment where you can feel confident in saying, "Oh, that's how it's done." This will tend to happen once you find those fundamental data structures, and have finally pieced together some understanding of how they all fit together. Once you've had the Aha! moment, you can start to trace the results back out, to make sure that is how the thing is accomplished, or what is returned. I do this with all large codebases I encounter that I want to understand. It's quite fun to do this with the Linux source code.

My philosophy is that "It's all just code", which means that with enough patience, it's all understandable. Sometimes a good strategy is to just start diving into it.

This is my approach too. I like to understand the entire flow, from the beginning to the end. To me this is the best way to get familiar because once you dive from different entry points you start noticing the patterns and similar paths in the code to the point where you don't need to dive to those areas again, as you quickly assimilate them by simply going over multiple times.
I find it frustrating that languages features work actively against you when you're trying to understand something.

Wide inheritance and macro usage are probably the worst. Good naming can aid understanding, but basic things like searchability are harmed by this.

Of those two, macros are the most trouble. You can't take anything for granted, and must look at every expression with an eye for detail. Taking notes becomes essential.

I also like to find a high level interface or function and follow down. Once I get to the boom, I then start following the important data. This is particularly helpful nowadays when data frequently moves between multiple systems before seeing easily visible results.
Interesting. I take a similar approach, but then I add testing (which usually coincides with fixing some bug).

Find an entry point to the system, then make it compile, then make the test run, then just keep on nudging the code until I'm satisfied I've covered what I'm interested in.

If I stay true to my mantra "only add test code when it is absolutely necessary" (is this argument needed? pass null and find out), I find an accurate (albeit not pretty) description the flow through that procedure.

Then you commit your test and save your discovery for posterity.

I use debuggers a lot for that purpose. It really helps to find the code paths for specific operations. Instead of reading code file by file, just setup a debugger, set a few breakpoints to the code, perform an operation and follow the read application code through through the paths.
At least to me there is no specific method, I work mainly with Java, since your specific case is JavaScript it may not even apply.

If the problem is some bug and there are stack traces that is my starting point, debugger and a few breakpoints chosen from the trace and then follow the stack and from there I start knowing how it is structured, and then the next bug and so on (fixing them of course) For code where I need to add features things get a little more tricky, but there is always some entry point, a web-service invocation, some web page, and try to understand what it is currently doing, again using the debugger to follow the calls and how the data is changed (sometimes even going into libraries).

Reading the docs if there are any is also a good place to start.

Once again, use the debugger a lot, makes it easier to understand than just reading the code.

(edit: formatting)

Get it running locally and then see what happens when you delete some stuff, especially stuff that you don't understand when reading through the code.
I've worked in a lot of legacy code bases. Here's my approach: * Skim around to get a general idea of what components are involved. * Try to understand that one module/class that keeps getting used a lot or is really important. * I mentally trace through that code, as if I'm a debugger. * Most importantly, I write down my discoveries/understanding as I go to help me retain this idea. * Re-skim with my new understanding and/or reorganize the code to be more concise or simpler. Depending on how ambitious you are, you might try to keep these changes. But with legacy code, it typically breaks as a result.

Every code base takes time to digest all the information. Sure the information passed your eyes, but is it committed to memory?

I just crack open the source base with Emacs, and start writing stuff down.

I use a large format (8x11 inch) notebook and start going through the abstractions file by file, filling up pages with summaries of things. I'll often copy out the major classes with a summary of their methods, and arrows to reflect class relationships. If there's a database involved, understanding what's being stored is usually pretty crucial, so I'll copy out the record definitions and make notes about fields. Call graphs and event diagrams go here, too.

After identifying the important stuff, I read code, and make notes about what the core functions and methods are doing. Here, a very fast global search is your friend, and "where is this declared?" and "who calls this?" are best answered in seconds. A source-base-wide grep works okay, but tools like Visual Assist's global search work better; I want answers fast.

Why use pen and paper? I find that this manual process helps my memory, and I can rapidly flip around in summaries that I've written in my own hand and fill in my understanding quite quickly. Usually, after a week or so I never refer to the notes again, but the initial phase of boosting my short term memory with paper, global searches and "getting my hands to know the code" works pretty well.

Also, I try to get the code running and fix a bug (or add a small feature) and check the change in, day one. I get anxious if I've been in a new code base for more than a few days without doing this.

I work similar to this. I love writing things in notebooks. I also like making diagrams on draw.io and printing them out for reference/writing on.
Totally agree with the point of pen/paper.

Something that compliments that approach is in-code annotation. Recently, I've recently been trying out https://github.com/bastibe/annotate.el which is pretty sweet. Check it out!

annotate.el looks pretty interesting, thank you.
Off topic, but anyone know what font and theme (it looks like the default theme but I'm not sure) are used in the project's screenshots?
The font is PragmataPro, which I am also using. Best font ever, but expensive.
I go as far as to have a dedicated project notebook for big new projects, I write everything down that I come across that I need to remember or need to question.

I've often been dropped into codebases where there is only a month to question the previous maintainer before all the business knowledge is lost as they move on to bigger and better things. So getting all the questions/queries down asap is the fastest step to get the undocumented business logic documented.

Even when you can't ask the questions I like to turn all the unknown unknowns into known unknowns. :)

Great points Also,

A list of the kinds of broad abstractions to look for might be useful;

* Each module and it's purpose

* Every global resource (whether global variables, message names, anything that the entire system has to deal with).

* The "style" that each coder used. Even "terrible" programmers tend to have a consistent approach and understanding that approach can make code much less opaque.

I also like to page though documents more quickly than my conscious mind can follow so as to get an unconscious feel for a code base. That might be just me.