548 lines
21 KiB
Markdown
548 lines
21 KiB
Markdown
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.
|