| Kati internals |
| ============== |
| |
| This is an informal document about internals of kati. This document is not meant |
| to be a comprehensive document of kati or GNU make. This explains some random |
| topics which other programmers may be interested in. |
| |
| Motivation |
| ---------- |
| |
| The motivation of kati was to speed up Android platform build. Especially, its |
| incremental build time was the main focus. Android platform's build system is a |
| very unique system. It provides a DSL, (ab)using Turing-completeness of GNU |
| make. The DSL allows developers to write build rules in a descriptive way, but |
| the downside is it's complicated and slow. |
| |
| When we say a build system is slow, we consider "null build" and "full |
| build". Null build is a build which does nothing, because all output files are |
| already up-to-date. Full build is a build which builds everything, because there |
| were nothing which have been already built. Actual builds in daily development |
| are somewhere between null build and full build. Most benchmarks below were done |
| for null build. |
| |
| For Android with my fairly beefy workstation, null build took ~100 secs with GNU |
| make. This means you needed to wait ~100 secs to see if there's a compile error |
| when you changed a single C file. To be fair, things were not that bad. There |
| are tools called mm/mmm. They allow developers to build an individual module. As |
| they ignore dependencies between modules, they are fast. However, you need to be |
| somewhat experienced to use them properly. You should know which modules will be |
| affected by your change. It would be nicer if you can just type "make" whenever |
| you change something. |
| |
| This is why we started this project. We decided to create a GNU make clone from |
| scratch, but there were some other options. One option was to replace all |
| Android.mk by files with a better format. There is actually a longer-term |
| project for this. Kati was planned to be a short-term project. Another option |
| was to hack GNU make instead of developing a clone. We didn't take this option |
| because we thought the source code of GNU make is somewhat complicated due to |
| historical reason. It's written in old-style C, has a lot of ifdefs for some |
| unknown architectures, etc. |
| |
| Currently, kati's main mode is --ninja mode. Instead of executing build commands |
| by itself, kati generates build.ninja file and |
| [ninja](https://github.com/martine/ninja) actually runs commands. There were |
| some back-and-forths before kati became the current form. Some experiments |
| succeeded and some others failed. We even changed the language for kati. At |
| first, we wrote kati in Go. We naively expected we can get enough performance |
| with Go. I guessed at least one of the following statements are true: 1. GNU |
| make is not very optimized for computation heavy Makefiles, 2. Go is fast for |
| our purpose, or 3. we can come up with some optimization tricks for Android's |
| build system. As for 3, some of such optimization succeeded but it's performance |
| gain didn't cancel the slowness of Go. |
| |
| Go's performance would be somewhat interesting topic. I didn't study the |
| performance difference in detail, but it seemed both our use of Go and Go |
| language itself were making the Go version of kati slower. As for our fault, I |
| think Go version has more unnecessary string allocations than C++ version |
| has. As for Go itself, it seemed GC was the main show-stopper. For example, |
| Android's build system defines about one million make variables, and buffers for |
| them will be never freed. IIRC, this kind of allocation pattern isn't good for |
| non-generational GC. |
| |
| Go version and test cases were written by ukai and me, and C++ rewrite was done |
| mostly by me. The rest of this document is mostly about the C++ version. |
| |
| Overall architecture |
| -------------------- |
| |
| Kati consists of the following components: |
| |
| * Parser |
| * Evaluator |
| * Dependency builder |
| * Executor |
| * Ninja generator |
| |
| A Makefile has some statements which consist of zero or more expressions. There |
| are two parsers and two evaluators - one for statements and the other for |
| expressions. |
| |
| Most of users of GNU make may not care about the evaluator much. However, GNU |
| make's evaluator is very powerful and is Turing-complete. For Android's null |
| build, most time is spent in this phase. Other tasks, such as building |
| dependency graphs and calling stat function for build targets, are not the |
| bottleneck. This would be a very Android specific characteristics. Android's |
| build system uses a lot of GNU make black magics. |
| |
| The evaluator outputs a list of build rules and a variable table. The dependency |
| builder creates a dependency graph from the list of build rules. Note this step |
| doesn't use the variable table. |
| |
| Then either executor or ninja generator will be used. Either way, kati runs its |
| evaluator again for command lines. The variable table is used again for this |
| step. |
| |
| We'll look at each components closely. GNU make is a somewhat different language |
| from modern languages. Let's see. |
| |
| Parser for statements |
| --------------------- |
| |
| I'm not 100% sure, but I think GNU make parses and evaluates Makefiles |
| simultaneously, but kati has two phases for parsing and evaluation. The reason |
| of this design is for performance. For Android build, kati (or GNU make) needs |
| to read ~3k files ~50k times. The file which is read most often is read ~5k |
| times. It's waste of time to parse such files again and again. Kati can re-use |
| parsed results when it needs to evaluate a Makefile second time. If we stop |
| caching the parsed results, kati will be two times slower for Android's |
| build. Caching parsed statements is done in *file_cache.cc*. |
| |
| The statement parser is defined in *parser.cc*. In kati, there are four kinds of |
| statements: |
| |
| * Rules |
| * Assignments |
| * Commands |
| * Make directives |
| |
| Data structures for them are defined in *stmt.h*. Here are examples of these |
| statements: |
| |
| VAR := yay! # An assignment |
| all: # A rule |
| echo $(VAR) # A command |
| include xxx.mk # A make directive (include) |
| |
| In addition to include directive, there are ifeq/ifneq/ifdef/ifndef directives |
| and export/unexport directives. Also, kati internally uses "parse error |
| statement". As GNU make doesn't show parse errors in branches which are not |
| taken, we need to delay parse errors to evaluation time. |
| |
| ### Context dependent parser |
| |
| A tricky point of parsing make statements is that the parsing depends on the |
| context of the evaluation. See the following Makefile chunk for example: |
| |
| $(VAR) |
| X=hoge echo $${X} |
| |
| You cannot tell whether the second line is a command or an assignment until |
| *$(VAR)* is evaluated. If *$(VAR)* is a rule statement, the second line is a |
| command and otherwise it's an assignment. If the previous line is |
| |
| VAR := target: |
| |
| the second line will turn out to be a command. |
| |
| For some reason, GNU make expands expressions before it decides the type of |
| a statement only for rules. Storing assignments or directives in a variable |
| won't work as assignments or directives. For example |
| |
| ASSIGN := A=B |
| $(ASSIGN): |
| |
| doesn't assign "*B:*" to *A*, but defines a build rule whose target is *A=B*. |
| |
| Anyway, as a line starts with a tab character can be either a command statement |
| or other statements depending on the evaluation result of the previous line, |
| sometimes kati's parser cannot tell the statement type of a line. In this case, |
| kati's parser speculatively creates a command statement object, keeping the |
| original line. If it turns out the line is actually not a command statement, |
| the evaluator re-runs the parser. |
| |
| ### Line concatenations and comments |
| |
| In most programming languages, line concatenations by a backslash character and |
| comments are handled at a very early stage of a language |
| implementation. However, GNU make changes the behavior for them depending on |
| parse/eval context. For example, the following Makefile outputs "has space" and |
| "hasnospace": |
| |
| VAR := has\ |
| space |
| all: |
| echo $(VAR) |
| echo has\ |
| nospace |
| |
| GNU make usually inserts a whitespace between lines, but for command lines it |
| doesn't. As we've seen in the previous subsection, sometimes kati cannot tell |
| a line is a command statement or not. This means we should handle them after |
| evaluating statements. Similar discussion applies for comments. GNU make usually |
| trims characters after '#', but it does nothing for '#' in command lines. |
| |
| We have a bunch of comment/backslash related testcases in the testcase directory |
| of kati's repository. |
| |
| Parser for expressions |
| ---------------------- |
| |
| A statement may have one or more expressions. The number of expressions in a |
| statement depends on the statement's type. For example, |
| |
| A := $(X) |
| |
| This is an assignment statement, which has two expressions - *A* and |
| *$(X)*. Types of expressions and their parser are defined in *expr.cc*. Like |
| other programming languages, an expression is a tree of expressions. The type of |
| a leaf expression is either literal, variable reference, |
| [substitution references](http://www.gnu.org/software/make/manual/make.html#Substitution-Refs), |
| or make functions. |
| |
| As written, backslashes and comments change their behavior depending on the |
| context. Kati handles them in this phase. *ParseExprOpt* is the enum for the |
| contexts. |
| |
| As a nature of old systems, GNU make is very permissive. For some reason, it |
| allows some kind of unmatched pairs of parentheses. For example, GNU make |
| doesn't think *$($(foo)* is an error - this is a reference to variable |
| *$(foo*. If you have some experiences with parsers, you may wonder how one can |
| implement a parser which allows such expressions. It seems GNU make |
| intentionally allows this: |
| |
| http://git.savannah.gnu.org/cgit/make.git/tree/expand.c#n285 |
| |
| No one won't use this feature intentionally. However, as GNU make allows this, |
| some Makefiles have unmatched parentheses, so kati shouldn't raise an error for |
| them, unfortunately. |
| |
| GNU make has a bunch of functions. Most users would use only simple ones such as |
| *$(wildcard ...)* and *$(subst ...)*. There are also more complex functions such |
| as *$(if ...)* and *$(call ...)*, which make GNU make Turing-complete. Make |
| functions are defined in *func.cc*. Though *func.cc* is not short, the |
| implementation is fairly simple. There is only one weirdness I remember around |
| functions. GNU make slightly changes its parsing for *$(if ...)*, *$(and ...)*, |
| and *$(or ...)*. See *trim_space* and *trim_right_space_1st* in *func.h* and how |
| they are used in *expr.cc*. |
| |
| Evaluator for statements |
| ------------------------ |
| |
| Evaluator for statements are defined in *eval.cc*. As written, there are four |
| kinds of statements: |
| |
| * Rules |
| * Assignments |
| * Commands |
| * Make directives |
| |
| There is nothing tricky around commands and make directives. A rule statement |
| have some forms and should be parsed after evaluating expression by the third |
| parser. This will be discussed in the next section. |
| |
| Assignments in GNU make is tricky a bit. There are two kinds of variables in GNU |
| make - simple variables and recursive variables. See the following code snippet: |
| |
| A = $(info world!) # recursive |
| B := $(info Hello,) # simple |
| $(A) |
| $(B) |
| |
| This code outputs "Hello," and "world!", in this order. The evaluation of |
| a recursive variable is delayed until the variable is referenced. So the first |
| line, which is an assignment of a recursive variable, outputs nothing. The |
| content of the variable *$(A)* will be *$(info world!)* after the first |
| line. The assignment in the second line uses *:=* which means this is a simple |
| variable assignment. For simple variables, the right hand side is evaluated |
| immediately. So "Hello," will be output and the value of *$(B)* will be an empty |
| string ($(info ...) returns an empty string). Then, "world!" will be shown when |
| the third line is evaluated as *$(A)* is evaluated, and lastly the forth line |
| does nothing, as *$(B)* is an empty string. |
| |
| There are two more kinds of assignments (i.e., *+=* and *?=*). These assignments |
| keep the type of the original variable. Evaluation of them will be done |
| immediately only when the left hand side of the assignment is already defined |
| and is a simple variable. |
| |
| Parser for rules |
| ---------------- |
| |
| After evaluating a rule statement, kati needs to parse the evaluated result. A |
| rule statement can actually be the following four things: |
| |
| * A rule |
| * A [target specific variable](http://www.gnu.org/software/make/manual/make.html#Target_002dspecific) |
| * An empty line |
| * An error (there're non-whitespace characters without a colon) |
| |
| Parsing them is mostly done in *rule.cc*. |
| |
| ### Rules |
| |
| A rule is something like *all: hello.exe*. You should be familiar with it. There |
| are several kinds of rules such as pattern rules, double colon rules, and order |
| only dependencies, but they don't complicate the rule parser. |
| |
| A feature which complicates the parser is semicolon. You can write the first |
| build command on the same line as the rule. For example, |
| |
| target: |
| echo hi! |
| |
| and |
| |
| target: ; echo hi! |
| |
| have the same meaning. This is tricky because kati shouldn't evaluate expressions |
| in a command until the command is actually invoked. As a semicolon can appear as |
| the result of expression evaluation, there are some corner cases. A tricky |
| example: |
| |
| all: $(info foo) ; $(info bar) |
| $(info baz) |
| |
| should output *foo*, *baz*, and then *bar*, in this order, but |
| |
| VAR := all: $(info foo) ; $(info bar) |
| $(VAR) |
| $(info baz) |
| |
| outputs *foo*, *bar*, and then *baz*. |
| |
| Again, for the command line after a semicolon, kati should also change how |
| backslashes and comments are handled. |
| |
| target: has\ |
| space ; echo no\ |
| space |
| |
| The above example says *target* depends on two targets, *has* and *space*, and |
| to build *target*, *echo nospace* should be executed. |
| |
| ### Target specific variables |
| |
| You may not familiar with target specific variables. This feature allows you to |
| define variable which can be referenced only from commands in a specified |
| target. See the following code: |
| |
| VAR := X |
| target1: VAR := Y |
| target1: |
| echo $(VAR) |
| target2: |
| echo $(VAR) |
| |
| In this example, *target1* shows *Y* and *target2* shows *X*. I think this |
| feature is somewhat similar to namespaces in other programming languages. If a |
| target specific variable is specified for a non-leaf target, the variable will |
| be used even in build commands of prerequisite targets. |
| |
| In general, I like GNU make, but this is the only GNU make's feature I don't |
| like. See the following Makefile: |
| |
| hello: CFLAGS := -g |
| hello: hello.o |
| gcc $(CFLAGS) $< -o $@ |
| hello.o: hello.c |
| gcc $(CFLAGS) -c $< -o $@ |
| |
| If you run make for the target *hello*, *CFLAGS* is applied for both commands: |
| |
| $ make hello |
| gcc -g -c hello.c -o hello.o |
| gcc -g hello.o -o hello |
| |
| However, *CFLAGS* for *hello* won't be used when you build only *hello.o*: |
| |
| $ make hello.o |
| gcc -c hello.c -o hello.o |
| |
| Things could be even worse when two targets with different target specific |
| variables depend on a same target. The build result will be inconsistent. I |
| think there is no valid usage of this feature for non-leaf targets. |
| |
| Let's go back to the parsing. Like for semicolons, we need to delay the |
| evaluation of the right hand side of the assignment for recursive variables. Its |
| implementation is very similar to the one for semicolons, but the combination of |
| the assignment and the semicolon makes parsing a bit trickier. An example: |
| |
| target1: ;X=Y echo $(X) # A rule with a command |
| target2: X=;Y echo $(X) # A target specific variable |
| |
| Evaluator for expressions |
| ------------------------- |
| |
| Evaluation of expressions is done in *expr.cc*, *func.cc*, and |
| *command.cc*. The amount of code for this step is fairly large especially |
| because of the number of GNU make functions. However, their implementations are |
| fairly straightforward. |
| |
| One tricky function is $(wildcard ...). It seems GNU make is doing some kind of |
| optimization only for this function and $(wildcard ...) in commands seem to be |
| evaluated before the evaluation phase for commands. Both C++ kati and Go kati |
| are different from GNU make's behavior in different ways, but it seems this |
| incompatibility is OK for Android build. |
| |
| There is an important optimization done for Android. Android's build system has |
| a lot of $(shell find ...) calls to create a list of all .java/.mk files under a |
| directory, and they are slow. For this, kati has a builtin emulator of GNU |
| find. The find emulator traverses the directory tree and creates an in-memory |
| directory tree. Then the find emulator returns results of find commands using |
| the cached tree. For my environment, the find command emulator makes kati ~1.6x |
| faster for AOSP. |
| |
| The implementations of some IO-related functions in commands are tricky in the |
| ninja generation mode. This will be described later. |
| |
| Dependency builder |
| ------------------ |
| |
| Now we get a list of rules and a variable table. *dep.cc* builds a dependency |
| graph using the list of rules. I think this step is what GNU make is supposed to |
| do for normal users. |
| |
| This step is fairly complex like other components but there's nothing |
| strange. There are three types of rules in GNU make: |
| |
| * explicit rule |
| * implicit rule |
| * suffix rule |
| |
| The following code shows the three types: |
| |
| all: foo.o |
| foo.o: |
| echo explicit |
| %.o: |
| echo implicit |
| .c.o: |
| echo suffix |
| |
| In the above example, all of these three rules match the target *foo.o*. GNU |
| make prioritizes explicit rules first. When there's no explicit rule for a |
| target, it uses an implicit rule with longer pattern string. Suffix rules are |
| used only when there are no explicit/implicit rules. |
| |
| Android has more than one thousand implicit rules and there are ten thousands of |
| targets. It's too slow to do matching for them with a naive O(NM) |
| algorithm. Kati uses a trie to speed up this step. |
| |
| Multiple rules without commands should be merged into the rule with a |
| command. For example: |
| |
| foo.o: foo.h |
| %.o: %.c |
| $(CC) -c $< -o $@ |
| |
| *foo.o* depends not only on *foo.c*, but also on *foo.h*. |
| |
| Executor |
| -------- |
| |
| C++ kati's executor is fairly simple. This is defined in *exec.cc*. This is |
| useful only for testing because this lacks some important features for a build |
| system (e.g., parallel build). |
| |
| Expressions in commands are evaluated at this stage. When they are evaluated, |
| target specific variables and some special variables (e.g., $< and $@) should be |
| considered. *command.cc* is handling them. This file is used by both the |
| executor and the ninja generator. |
| |
| Evaluation at this stage is tricky when both *+=* and target specific variables |
| are involved. Here is an example code: |
| |
| all: test1 test2 test3 test4 |
| |
| A:=X |
| B=X |
| X:=foo |
| |
| test1: A+=$(X) |
| test1: |
| @echo $(A) # X bar |
| |
| test2: B+=$(X) |
| test2: |
| @echo $(B) # X bar |
| |
| test3: A:= |
| test3: A+=$(X) |
| test3: |
| @echo $(A) # foo |
| |
| test4: B= |
| test4: B+=$(X) |
| test4: |
| @echo $(B) # bar |
| |
| X:=bar |
| |
| *$(A)* in *test3* is a simple variable. Though *$(A)* in the global scope is |
| simple, *$(A)* in *test1* is a recursive variable. This means types of global |
| variables don't affect types of target specific variables. However, The result |
| of *test1* ("X bar") shows the value of a target specific variable is |
| concatenated to the value of a global variable. |
| |
| Ninja generator |
| --------------- |
| |
| *ninja.cc* generates a ninja file using the results of other components. This |
| step is actually fairly complicated because kati needs to map GNU make's |
| features to ninja's. |
| |
| A build rule in GNU make may have multiple commands, while ninja's has always a |
| single command. To mitigate this, the ninja generator translates multiple |
| commands into something like *(cmd1) && (cmd2) && ...*. Kati should also escape |
| some special characters for ninja and shell. |
| |
| The tougher thing is $(shell ...) in commands. Current kati's implementation |
| translates it into shell's $(...). This works for many cases. But this approach |
| won't work when the result of $(shell ...) is passed to another make |
| function. For example |
| |
| all: |
| echo $(if $(shell echo),FAIL,PASS) |
| |
| should output PASS, because the result of $(shell echo) is an empty string. GNU |
| make and kati's executor mode output PASS correctly. However, kati's ninja |
| generator emits a ninja file which shows FAIL. |
| |
| I wrote a few experimental patches for this issue, but they didn't |
| work well. The current kati's implementation has an Android specific workaround |
| for this. See *HasNoIoInShellScript* in *func.cc* for detail. |
| |
| Ninja regeneration |
| ------------------ |
| |
| C++ kati has --regen flag. If this flag is specified, kati checks if anything |
| in your environment was changed after the previous run. If kati thinks it doesn't |
| need to regenerate the ninja file, it finishes quickly. For Android, running |
| kati takes ~30 secs at the first run but the second run takes only ~1 sec. |
| |
| Kati thinks it needs to regenerate the ninja file when one of the followings is |
| changed: |
| |
| * The command line flags passed to kati |
| * A timestamp of a Makefile used to generate the previous ninja file |
| * An environment variable used while evaluating Makefiles |
| * A result of $(wildcard ...) |
| * A result of $(shell ...) |
| |
| Quickly doing the last check is not trivial. It takes ~18 secs to run all |
| $(shell ...) in Android's build system due to the slowness of $(shell find |
| ...). So, for find commands executed by kati's find emulator, kati stores the |
| timestamps of traversed directories with the find command itself. For each find |
| commands, kati checks the timestamps of them. If they are not changed, kati |
| skips re-running the find command. |
| |
| Kati doesn't run $(shell date ...) and $(shell echo ...) during this check. The |
| former always changes so there's no sense to re-run them. Android uses the |
| latter to create a file and the result of them are empty strings. We don't want |
| to update these files to get empty strings. |
| |
| TODO |
| ---- |
| |
| A big TODO is sub-makes invoked by $(MAKE). I wrote some experimental patches |
| but nothing is ready to be used as of writing. |