Skip to content

Commit 874b3b6

Browse files
Cache the results of include detection
This greatly reduces compilation time of big sketches (or sketches using big libraries) when only a small change has been mad. Instead of rerunning include detection for *all* source files, it is now only rerun for changed files (and usually more if the actual list of includes changed). Signed-off-by: Matthijs Kooijman <matthijs@stdin.nl>
1 parent 8b70ca7 commit 874b3b6

File tree

2 files changed

+215
-17
lines changed

2 files changed

+215
-17
lines changed

src/arduino.cc/builder/constants/constants.go

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const FILE_PLATFORM_KEYS_REWRITE_TXT = "platform.keys.rewrite.txt"
8484
const FILE_PLATFORM_LOCAL_TXT = "platform.local.txt"
8585
const FILE_PLATFORM_TXT = "platform.txt"
8686
const FILE_PROGRAMMERS_TXT = "programmers.txt"
87+
const FILE_INCLUDES_CACHE = "includes.cache"
8788
const FOLDER_BOOTLOADERS = "bootloaders"
8889
const FOLDER_CORE = "core"
8990
const FOLDER_CORES = "cores"
@@ -180,6 +181,7 @@ const MSG_USING_LIBRARY = "Using library {0} in folder: {1} {2}"
180181
const MSG_USING_BOARD = "Using board '{0}' from platform in folder: {1}"
181182
const MSG_USING_CORE = "Using core '{0}' from platform in folder: {1}"
182183
const MSG_USING_PREVIOUS_COMPILED_FILE = "Using previously compiled file: {0}"
184+
const MSG_USING_CACHED_INCLUDES = "Using cached library dependencies for file: {0}"
183185
const MSG_WARNING_LIB_INVALID_CATEGORY = "WARNING: Category '{0}' in library {1} is not valid. Setting to '{2}'"
184186
const MSG_WARNING_PLATFORM_MISSING_VALUE = "Warning: platform.txt from core '{0}' misses property '{1}', using default value '{2}'. Consider upgrading this core."
185187
const MSG_WARNING_PLATFORM_OLD_VALUES = "Warning: platform.txt from core '{0}' contains deprecated {1}, automatically converted to {2}. Consider upgrading this core."

src/arduino.cc/builder/container_find_includes.go

+213-17
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,61 @@ be processed as well. When the preprocessor completes without showing an
5959
the next. When no library can be found for a included filename, an error
6060
is shown and the process is aborted.
6161
62+
Caching
63+
64+
Since this process is fairly slow (requiring at least one invocation of
65+
the preprocessor per source file), its results are cached.
66+
67+
Just caching the complete result (i.e. the resulting list of imported
68+
libraries) seems obvious, but such a cache is hard to invalidate. Making
69+
a list of all the source and header files used to create the list and
70+
check if any of them changed is probably feasible, but this would also
71+
require caching the full list of libraries to invalidate the cache when
72+
the include to library resolution might have a different result. Another
73+
downside of a complete cache is that any changes requires re-running
74+
everything, even if no includes were actually changed.
75+
76+
Instead, caching happens by keeping a sort of "journal" of the steps in
77+
the include detection, essentially tracing each file processed and each
78+
include path entry added. The cache is used by retracing these steps:
79+
The include detection process is executed normally, except that instead
80+
of running the preprocessor, the include filenames are (when possible)
81+
read from the cache. Then, the include file to library resolution is
82+
again executed normally. The results are checked against the cache and
83+
as long as the results match, the cache is considered valid.
84+
85+
When a source file (or any of the files it includes, as indicated by the
86+
.d file) is changed, the preprocessor is executed as normal for the
87+
file, ignoring any includes from the cache. This does not, however,
88+
invalidate the cache: If the results from the preprocessor match the
89+
entries in the cache, the cache remains valid and can again be used for
90+
the next (unchanged) file.
91+
92+
The cache file uses the JSON format and contains a list of entries. Each
93+
entry represents a discovered library and contains:
94+
- Sourcefile: The source file that the include was found in
95+
- Include: The included filename found
96+
- Includepath: The addition to the include path
97+
98+
There are also some special entries:
99+
- When adding the initial include path entries, such as for the core
100+
and variant paths. These are not discovered, so the Sourcefile and
101+
Include fields will be empty.
102+
- When a file contains no (more) missing includes, an entry with an
103+
empty Include and IncludePath is generated.
104+
62105
*/
63106

64107
package builder
65108

66109
import (
110+
"encoding/json"
111+
"io/ioutil"
67112
"os"
68113
"path/filepath"
114+
"time"
69115

116+
"arduino.cc/builder/builder_utils"
70117
"arduino.cc/builder/constants"
71118
"arduino.cc/builder/i18n"
72119
"arduino.cc/builder/types"
@@ -76,9 +123,12 @@ import (
76123
type ContainerFindIncludes struct{}
77124

78125
func (s *ContainerFindIncludes) Run(ctx *types.Context) error {
79-
appendIncludeFolder(ctx, ctx.BuildProperties[constants.BUILD_PROPERTIES_BUILD_CORE_PATH])
126+
cachePath := filepath.Join(ctx.BuildPath, constants.FILE_INCLUDES_CACHE)
127+
cache := readCache(cachePath)
128+
129+
appendIncludeFolder(ctx, cache, "", "", ctx.BuildProperties[constants.BUILD_PROPERTIES_BUILD_CORE_PATH])
80130
if ctx.BuildProperties[constants.BUILD_PROPERTIES_BUILD_VARIANT_PATH] != constants.EMPTY_STRING {
81-
appendIncludeFolder(ctx, ctx.BuildProperties[constants.BUILD_PROPERTIES_BUILD_VARIANT_PATH])
131+
appendIncludeFolder(ctx, cache, "", "", ctx.BuildProperties[constants.BUILD_PROPERTIES_BUILD_VARIANT_PATH])
82132
}
83133

84134
sketch := ctx.Sketch
@@ -96,12 +146,20 @@ func (s *ContainerFindIncludes) Run(ctx *types.Context) error {
96146
}
97147

98148
for !sourceFilePaths.Empty() {
99-
err := findIncludesUntilDone(ctx, sourceFilePaths.Pop())
149+
err := findIncludesUntilDone(ctx, cache, sourceFilePaths.Pop())
100150
if err != nil {
151+
os.Remove(cachePath)
101152
return i18n.WrapError(err)
102153
}
103154
}
104155

156+
// Finalize the cache
157+
cache.ExpectEnd()
158+
err = writeCache(cache, cachePath)
159+
if err != nil {
160+
return i18n.WrapError(err)
161+
}
162+
105163
err = runCommand(ctx, &FailIfImportedLibraryIsWrong{})
106164
if err != nil {
107165
return i18n.WrapError(err)
@@ -110,9 +168,14 @@ func (s *ContainerFindIncludes) Run(ctx *types.Context) error {
110168
return nil
111169
}
112170

113-
// Append the given folder to the include path.
114-
func appendIncludeFolder(ctx *types.Context, folder string) {
171+
// Append the given folder to the include path and match or append it to
172+
// the cache. sourceFilePath and include indicate the source of this
173+
// include (e.g. what #include line in what file it was resolved from)
174+
// and should be the empty string for the default include folders, like
175+
// the core or variant.
176+
func appendIncludeFolder(ctx *types.Context, cache *includeCache, sourceFilePath string, include string, folder string) {
115177
ctx.IncludeFolders = append(ctx.IncludeFolders, folder)
178+
cache.ExpectEntry(sourceFilePath, include, folder)
116179
}
117180

118181
func runCommand(ctx *types.Context, command types.Command) error {
@@ -124,25 +187,157 @@ func runCommand(ctx *types.Context, command types.Command) error {
124187
return nil
125188
}
126189

127-
func findIncludesUntilDone(ctx *types.Context, sourceFile types.SourceFile) error {
190+
type includeCacheEntry struct {
191+
Sourcefile string
192+
Include string
193+
Includepath string
194+
}
195+
196+
type includeCache struct {
197+
// Are the cache contents valid so far?
198+
valid bool
199+
// Index into entries of the next entry to be processed. Unused
200+
// when the cache is invalid.
201+
next int
202+
entries []includeCacheEntry
203+
}
204+
205+
// Return the next cache entry. Should only be called when the cache is
206+
// valid and a next entry is available (the latter can be checked with
207+
// ExpectFile). Does not advance the cache.
208+
func (cache *includeCache) Next() includeCacheEntry {
209+
return cache.entries[cache.next]
210+
}
211+
212+
// Check that the next cache entry is about the given file. If it is
213+
// not, or no entry is available, the cache is invalidated. Does not
214+
// advance the cache.
215+
func (cache *includeCache) ExpectFile(sourcefile string) {
216+
if cache.valid && cache.next < len(cache.entries) && cache.Next().Sourcefile != sourcefile {
217+
cache.valid = false
218+
cache.entries = cache.entries[:cache.next]
219+
}
220+
}
221+
222+
// Check that the next entry matches the given values. If so, advance
223+
// the cache. If not, the cache is invalidated. If the cache is
224+
// invalidated, or was already invalid, an entry with the given values
225+
// is appended.
226+
func (cache *includeCache) ExpectEntry(sourcefile string, include string, librarypath string) {
227+
entry := includeCacheEntry{Sourcefile: sourcefile, Include: include, Includepath: librarypath}
228+
if cache.valid {
229+
if cache.next < len(cache.entries) && cache.Next() == entry {
230+
cache.next++
231+
} else {
232+
cache.valid = false
233+
cache.entries = cache.entries[:cache.next]
234+
}
235+
}
236+
237+
if !cache.valid {
238+
cache.entries = append(cache.entries, entry)
239+
}
240+
}
241+
242+
// Check that the cache is completely consumed. If not, the cache is
243+
// invalidated.
244+
func (cache *includeCache) ExpectEnd() {
245+
if cache.valid && cache.next < len(cache.entries) {
246+
cache.valid = false
247+
cache.entries = cache.entries[:cache.next]
248+
}
249+
}
250+
251+
// Read the cache from the given file
252+
func readCache(path string) *includeCache {
253+
bytes, err := ioutil.ReadFile(path)
254+
if err != nil {
255+
// Return an empty, invalid cache
256+
return &includeCache{}
257+
}
258+
result := &includeCache{}
259+
err = json.Unmarshal(bytes, &result.entries)
260+
if err != nil {
261+
// Return an empty, invalid cache
262+
return &includeCache{}
263+
}
264+
result.valid = true
265+
return result
266+
}
267+
268+
// Write the given cache to the given file if it is invalidated. If the
269+
// cache is still valid, just update the timestamps of the file.
270+
func writeCache(cache *includeCache, path string) error {
271+
// If the cache was still valid all the way, just touch its file
272+
// (in case any source file changed without influencing the
273+
// includes). If it was invalidated, overwrite the cache with
274+
// the new contents.
275+
if cache.valid {
276+
os.Chtimes(path, time.Now(), time.Now())
277+
} else {
278+
bytes, err := json.MarshalIndent(cache.entries, "", " ")
279+
if err != nil {
280+
return i18n.WrapError(err)
281+
}
282+
err = utils.WriteFileBytes(path, bytes)
283+
if err != nil {
284+
return i18n.WrapError(err)
285+
}
286+
}
287+
return nil
288+
}
289+
290+
func findIncludesUntilDone(ctx *types.Context, cache *includeCache, sourceFile types.SourceFile) error {
291+
sourcePath := sourceFile.SourcePath(ctx)
128292
targetFilePath := utils.NULLFile()
293+
294+
// TODO: This should perhaps also compare against the
295+
// include.cache file timestamp. Now, it only checks if the file
296+
// changed after the object file was generated, but if it
297+
// changed between generating the cache and the object file,
298+
// this could show the file as unchanged when it really is
299+
// changed. Changing files during a build isn't really
300+
// supported, but any problems from it should at least be
301+
// resolved when doing another build, which is not currently the
302+
// case.
303+
// TODO: This reads the dependency file, but the actual building
304+
// does it again. Should the result be somehow cached? Perhaps
305+
// remove the object file if it is found to be stale?
306+
unchanged, err := builder_utils.ObjFileIsUpToDate(sourcePath, sourceFile.ObjectPath(ctx), sourceFile.DepfilePath(ctx))
307+
if err != nil {
308+
return i18n.WrapError(err)
309+
}
310+
311+
first := true
129312
for {
130-
commands := []types.Command{
131-
&GCCPreprocRunnerForDiscoveringIncludes{SourceFilePath: sourceFile.SourcePath(ctx), TargetFilePath: targetFilePath},
132-
&IncludesFinderWithRegExp{Source: &ctx.SourceGccMinusE},
133-
}
134-
for _, command := range commands {
135-
err := runCommand(ctx, command)
136-
if err != nil {
137-
return i18n.WrapError(err)
313+
var include string
314+
cache.ExpectFile(sourcePath)
315+
if unchanged && cache.valid {
316+
include = cache.Next().Include
317+
if first && ctx.Verbose {
318+
ctx.GetLogger().Println(constants.LOG_LEVEL_INFO, constants.MSG_USING_CACHED_INCLUDES, sourcePath)
138319
}
320+
} else {
321+
commands := []types.Command{
322+
&GCCPreprocRunnerForDiscoveringIncludes{SourceFilePath: sourcePath, TargetFilePath: targetFilePath},
323+
&IncludesFinderWithRegExp{Source: &ctx.SourceGccMinusE},
324+
}
325+
for _, command := range commands {
326+
err := runCommand(ctx, command)
327+
if err != nil {
328+
return i18n.WrapError(err)
329+
}
330+
}
331+
include = ctx.IncludeJustFound
139332
}
140-
if ctx.IncludeJustFound == "" {
333+
334+
if include == "" {
141335
// No missing includes found, we're done
336+
cache.ExpectEntry(sourcePath, "", "")
142337
return nil
143338
}
144339

145-
library := ResolveLibrary(ctx, ctx.IncludeJustFound)
340+
library := ResolveLibrary(ctx, include)
146341
if library == nil {
147342
// Library could not be resolved, show error
148343
err := runCommand(ctx, &GCCPreprocRunner{TargetFileName: constants.FILE_CTAGS_TARGET_FOR_GCC_MINUS_E})
@@ -153,11 +348,12 @@ func findIncludesUntilDone(ctx *types.Context, sourceFile types.SourceFile) erro
153348
// include path and queue its source files for further
154349
// include scanning
155350
ctx.ImportedLibraries = append(ctx.ImportedLibraries, library)
156-
appendIncludeFolder(ctx, library.SrcFolder)
351+
appendIncludeFolder(ctx, cache, sourcePath, include, library.SrcFolder)
157352
sourceFolders := types.LibraryToSourceFolder(library)
158353
for _, sourceFolder := range sourceFolders {
159354
queueSourceFilesFromFolder(ctx, ctx.CollectedSourceFiles, library, sourceFolder.Folder, sourceFolder.Recurse)
160355
}
356+
first = false
161357
}
162358
}
163359

0 commit comments

Comments
 (0)