@@ -59,14 +59,61 @@ be processed as well. When the preprocessor completes without showing an
59
59
the next. When no library can be found for a included filename, an error
60
60
is shown and the process is aborted.
61
61
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
+
62
105
*/
63
106
64
107
package builder
65
108
66
109
import (
110
+ "encoding/json"
111
+ "io/ioutil"
67
112
"os"
68
113
"path/filepath"
114
+ "time"
69
115
116
+ "arduino.cc/builder/builder_utils"
70
117
"arduino.cc/builder/constants"
71
118
"arduino.cc/builder/i18n"
72
119
"arduino.cc/builder/types"
@@ -76,9 +123,12 @@ import (
76
123
type ContainerFindIncludes struct {}
77
124
78
125
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 ])
80
130
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 ])
82
132
}
83
133
84
134
sketch := ctx .Sketch
@@ -96,12 +146,20 @@ func (s *ContainerFindIncludes) Run(ctx *types.Context) error {
96
146
}
97
147
98
148
for ! sourceFilePaths .Empty () {
99
- err := findIncludesUntilDone (ctx , sourceFilePaths .Pop ())
149
+ err := findIncludesUntilDone (ctx , cache , sourceFilePaths .Pop ())
100
150
if err != nil {
151
+ os .Remove (cachePath )
101
152
return i18n .WrapError (err )
102
153
}
103
154
}
104
155
156
+ // Finalize the cache
157
+ cache .ExpectEnd ()
158
+ err = writeCache (cache , cachePath )
159
+ if err != nil {
160
+ return i18n .WrapError (err )
161
+ }
162
+
105
163
err = runCommand (ctx , & FailIfImportedLibraryIsWrong {})
106
164
if err != nil {
107
165
return i18n .WrapError (err )
@@ -110,9 +168,14 @@ func (s *ContainerFindIncludes) Run(ctx *types.Context) error {
110
168
return nil
111
169
}
112
170
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 ) {
115
177
ctx .IncludeFolders = append (ctx .IncludeFolders , folder )
178
+ cache .ExpectEntry (sourceFilePath , include , folder )
116
179
}
117
180
118
181
func runCommand (ctx * types.Context , command types.Command ) error {
@@ -124,25 +187,157 @@ func runCommand(ctx *types.Context, command types.Command) error {
124
187
return nil
125
188
}
126
189
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 )
128
292
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
129
312
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 )
138
319
}
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
139
332
}
140
- if ctx .IncludeJustFound == "" {
333
+
334
+ if include == "" {
141
335
// No missing includes found, we're done
336
+ cache .ExpectEntry (sourcePath , "" , "" )
142
337
return nil
143
338
}
144
339
145
- library := ResolveLibrary (ctx , ctx . IncludeJustFound )
340
+ library := ResolveLibrary (ctx , include )
146
341
if library == nil {
147
342
// Library could not be resolved, show error
148
343
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
153
348
// include path and queue its source files for further
154
349
// include scanning
155
350
ctx .ImportedLibraries = append (ctx .ImportedLibraries , library )
156
- appendIncludeFolder (ctx , library .SrcFolder )
351
+ appendIncludeFolder (ctx , cache , sourcePath , include , library .SrcFolder )
157
352
sourceFolders := types .LibraryToSourceFolder (library )
158
353
for _ , sourceFolder := range sourceFolders {
159
354
queueSourceFilesFromFolder (ctx , ctx .CollectedSourceFiles , library , sourceFolder .Folder , sourceFolder .Recurse )
160
355
}
356
+ first = false
161
357
}
162
358
}
163
359
0 commit comments