@@ -59,14 +59,61 @@ be processed as well. When the preprocessor completes without showing an
5959the next. When no library can be found for a included filename, an error
6060is 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
64107package builder
65108
66109import (
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 (
76123type ContainerFindIncludes struct {}
77124
78125func (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
118181func 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