Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include cache fixes and refactorings #236

Closed

Conversation

matthijskooijman
Copy link
Collaborator

This PR fixes a few corner cases in the include caching and refactors some code in the process. In particular, this fixes the issue reported at #230. The problem in that report consisted of three parts:

  • arduino-builder would fail to remove the -MMD option from commandline for the chipKIT32 boards, due to the way their platform.txt was structured. This caused the preprocessor command to fail, trying to write to /dev/null.d.
  • The preprocessor command would be run a second time to show an error, but with slightly different options, which prevented the error from occuring. This caused arduino-builder to ignore the failure and continue, masking the above problem.
  • Because the include cache would now be in a half-finished state, a small bug in the include caching was triggered that caused an out-of-bounds array read and a panic.

This PR fixes all three problems (in reverse order), with a fair amount of refactoring in between (some of which is strictly needed, some of which is "while I'm here").

@ArduinoBot
Copy link
Contributor

✅ Build completed.

⬇️ Build URL: http://downloads.arduino.cc/PR/arduino-builder/arduino-builder-236.zip

ℹ️ To test this build:

  1. Replace arduino-builder binary (you can find it where you installed the IDE) with the provided one

Copy link
Member

@facchinm facchinm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall it looks really good, particularly generalisation through ExecCommand function.

Anyway, a lot of functions turned into using the global context; while the new signatures are slightly more readable, unit-testing could be much more convoluted but I don't have a strong opinion about it.

@matthijskooijman
Copy link
Collaborator Author

Anyway, a lot of functions turned into using the global context; while the new signatures are slightly more readable, unit-testing could be much more convoluted but I don't have a strong opinion about it.

The policy I'm using here is that these helper functions can access things like ctx.DebugLevel and ctx.Verbose, which are essentially just global flags, but should still use explicit arguments for things like the list of files to compiler, or the buildProperties to use (even when these are also available through the context). In the long run, I think the latter should actually be removed from the context and be explicitely passed from function to function instead, to reduce the amount of implicit dataflow.

@matthijskooijman
Copy link
Collaborator Author

@facchinm pointed out some benchmarks that show this PR significantly slows down compilation. This needs some investigation before merging this PR.

@ArduinoBot
Copy link
Contributor

❌ Build failed.

@matthijskooijman
Copy link
Collaborator Author

I can't really reproduce the slow benchmarks of #242. I tried PR232, this PR, and this PR after rebasing, which all give similar times (there's a bit of variation, but I was using my computer while running these texts, so that probably explains the small differences).

                                                PR232    PR236    PR236 rebased
TestTryBuild001   ok    arduino.cc/builder/test 2.398s   2.103s   2.281s
TestTryBuild002   ok    arduino.cc/builder/test 2.081s   2.022s   1.977s
TestTryBuild003   ok    arduino.cc/builder/test 0.124s   0.133s   0.129s
TestTryBuild004   ok    arduino.cc/builder/test 1.997s   2.018s   1.982s
TestTryBuild005   ok    arduino.cc/builder/test 2.275s   2.162s   2.236s
TestTryBuild006   ok    arduino.cc/builder/test 2.105s   1.979s   2.094s
TestTryBuild007   ok    arduino.cc/builder/test 3.393s   3.375s   3.391s
TestTryBuild009   ok    arduino.cc/builder/test 2.039s   2.038s   2.030s
TestTryBuild010   ok    arduino.cc/builder/test 2.083s   2.038s   1.954s
TestTryBuild011   ok    arduino.cc/builder/test 2.008s   2.051s   2.043s
TestTryBuild012   ok    arduino.cc/builder/test 2.012s   2.043s   2.043s
TestTryBuild013   ok    arduino.cc/builder/test 2.066s   2.005s   2.077s
TestTryBuild014   ok    arduino.cc/builder/test 2.008s   2.006s   2.049s
TestTryBuild015   ok    arduino.cc/builder/test 2.351s   2.238s   2.287s
TestTryBuild017   ok    arduino.cc/builder/test 0.206s   0.320s   0.173s
TestTryBuild018   ok    arduino.cc/builder/test 2.242s   3.001s   2.258s
TestTryBuild019   ok    arduino.cc/builder/test 2.020s   2.626s   1.976s
TestTryBuild020   ok    arduino.cc/builder/test 0.221s   0.316s   0.221s
TestTryBuild021   ok    arduino.cc/builder/test 2.105s   2.370s   2.003s
TestTryBuild022   ok    arduino.cc/builder/test 9.257s   10.876s  9.241s
TestTryBuild023   ok    arduino.cc/builder/test 2.023s   2.429s   2.303s
TestTryBuild024   ok    arduino.cc/builder/test 1.999s   2.000s   2.627s
TestTryBuild026   ok    arduino.cc/builder/test 2.578s   2.655s   3.495s
TestTryBuild027   ok    arduino.cc/builder/test 2.307s   2.305s   2.600s
TestTryBuild028   ok    arduino.cc/builder/test 2.051s   2.160s   2.429s
TestTryBuild029   ok    arduino.cc/builder/test 2.069s   1.980s   2.721s
TestTryBuild030   ok    arduino.cc/builder/test 1.982s   2.191s   2.493s
TestTryBuild031   ok    arduino.cc/builder/test 3.461s   3.398s   4.421s
TestTryBuild032   ok    arduino.cc/builder/test 2.130s   2.211s   2.129s
TestTryBuild033   ok    arduino.cc/builder/test 1.960s   2.044s   1.999s
TestTryBuild034   ok    arduino.cc/builder/test 1.990s   2.039s   2.055s
TestTryBuild035   ok    arduino.cc/builder/test 1.973s   1.963s   2.007s
TestTryBuild036   ok    arduino.cc/builder/test 8.696s   8.847s   9.243s
TestTryBuild037   ok    arduino.cc/builder/test 2.003s   1.989s   2.010s
TestTryBuild038   ok    arduino.cc/builder/test 2.008s   1.978s   2.002s
TestTryBuild039   ok    arduino.cc/builder/test 5.239s   5.788s   5.368s
TestTryBuild040   ok    arduino.cc/builder/test 2.017s   2.588s   1.969s
TestTryBuild041   ok    arduino.cc/builder/test 1.990s   2.696s   1.977s
TestTryBuild042   ok    arduino.cc/builder/test 1.973s   2.478s   1.960s

For testing, I used for i in TestTryBuild{001..042}; do echo -ne "$i "; go test arduino.cc/builder/test -run $i;done

@facchinm, So it seems this PR can be merged? I'll ask in the other PR to see if those benchmarks can be reproduced, just in case, but I don't think this should block this PR any longer. I've also just rebased this PR on top of master, and re-ran the test suite, which still works.

@ArduinoBot
Copy link
Contributor

✅ Build completed.

⬇️ Build URL: http://downloads.arduino.cc/PR/arduino-builder/arduino-builder-236.zip

ℹ️ To test this build:

  1. Replace arduino-builder binary (you can find it where you installed the IDE) with the provided one

@matthijskooijman
Copy link
Collaborator Author

I also added three more cleanup commits from #217 (since that PR isn't quite ready, but those cleanup commits are fine to merge already).

@ArduinoBot
Copy link
Contributor

✅ Build completed.

⬇️ Build URL: http://downloads.arduino.cc/PR/arduino-builder/arduino-builder-236.zip

ℹ️ To test this build:

  1. Replace arduino-builder binary (you can find it where you installed the IDE) with the provided one

@matthijskooijman
Copy link
Collaborator Author

@facchinm, it indeed seems like the slowness of this PR was a one-off glitch, see #242 (comment)

The comments state that if ExpectFile() is called, but there are no
remaining items in the cache, it will invalidate the cache. However, the
code would only invalidate the cache if at least one item was still
present, but it didn't match the expected file. In practice, this
wouldn't usually cause issues, since adding a new file would usually
cause an invalid cache earlier on, and even if a new file was added at
the end of the compilation, it would not be in the .d file, so it would
be marked as "changed".

However, in rare circumstances, such as when the include cache would not
be properly generated due to other problems (see arduino#230), this would cause
a crash, when ExpectFile did not invalidate the cache and the file in
question was unchanged, causing an out-of-bounds read from the cache.

This commit fixes this by making ExpectFile behave like documented and
invalidate the cache when there are no remaining entries.

Signed-off-by: Matthijs Kooijman <[email protected]>
Previously, the relative filename passed to GCCPreprocRunner() was made
absolute by prepareGCCPreprocRecipeProperties() and then returned, so it
could be used later on. There was an exception to this when /dev/null
was passed.

Now, the only place that passed something other than /dev/null simply
does this processing up front instead. This prepares the way for
removing Context::FileToRead in the next commit.

Signed-off-by: Matthijs Kooijman <[email protected]>
Previously, this filename was set by GCCPreprocRunner into the context,
because the full filename was not known until then. Since the previous
commit, this filename is known by the ContainerAddPrototypes, which can
just pass it to ReadFileAndStoreInContext explicitly. This allows
Context::FileToRead to be removed.

Signed-off-by: Matthijs Kooijman <[email protected]>
This pass was unused.

Signed-off-by: Matthijs Kooijman <[email protected]>
Previously, the verbose, logger and sometimes warningFlags were
extracted from the Context by the top-level runner and passed down
separately. Since this causes a lot of variable-passing of what is
essentially global state, it is clearer to just pass a types.Context
down and let the helpers get the data they need from that state.

This prepared for a next commit where ExecRecipe will be refactored and
needs access to the Context. Since the next commit will heavily change
ExecRecipe anyway, this commit does not actually change ExecRecipe to
accept the Context.

This commit should not change any behaviour.

Signed-off-by: Matthijs Kooijman <[email protected]>
For some reason the sizer commandline was never shown. For consistency,
it is now shown in verbose mode, just like the other commands.

Signed-off-by: Matthijs Kooijman <[email protected]>
This also happens with the normal compilation commands, so why not with
these? Normally these commands should not output to stdout, so this
doesn't make any difference, but it makes things more consistent.

Signed-off-by: Matthijs Kooijman <[email protected]>
Previously, this function would ignore any errors returned by `Run()`
since the command is expected to fail in most cases. However, in
addition to ignoring a non-zero exit code from the command, it would
also ignore errors in running the command itself.

With this commit, `ExecRecipeCollectStdErr()` simply returns all errors,
but its caller checks the type of the error. If it is `ExitError`, this
indicates a non-zero exit status, which is ignored. Otherwise, the error
is reported as normal.

Signed-off-by: Matthijs Kooijman <[email protected]>
Previously, the return value was converted to string. Letting callers
convert to string makes it easier to write the sterr contents to
os.Stderr again in a future commit.

Signed-off-by: Matthijs Kooijman <[email protected]>
For include detection, the preprocessor is run on all source files,
collecting #included filenames from the stderr output, each of which are
then resolved to a library to include. A caching mechanism is used to
only run the preprocessor when needed.

This commit improves the error handling during include detection in a
number of ways:
 - When the preprocessor runs succesfully, processing stops for the
   current file. Previously, it would always look at stderr to find a
   missing include filename and only stop if none was found.
 - When the preprocessor fails, but no filename can be found, show the
   error preprocessor error. Previously, it would assume that the
   process was done and stop processing the file without any error.
 - When no library can be found for a missing include, show the stored
   error output instead of running the preprocessor again. Previously,
   the preprocessor would be run a second time, to (re)generate the
   error message.

   When the include filename comes from the cache and the preprocessor
   was not run yet, it is still run to show its errors to the user. This
   should be very rare, as normally changes that cause a cached filename
   to become unresolvable to a library also cause the file to be marked
   as changed, bypassing the cache. When this does happen, the
   preprocessor is now run using `GCCPreprocRunnerForDiscoveringIncludes()`
   instead of `GCCPreprocRunner()`, which ensures the preprocessor
   command is always exactly the same.

   Before this change, there could be specific circumstances where the
   first preprocessor run would generate an error, but where the second
   run would not show the error and include detection would continue as
   if nothing happened. One such circumstance is described in arduino#230.

Signed-off-by: Matthijs Kooijman <[email protected]>
This unifies these similar methods into a single method. The interface
is additionally changed to:
 - Accepts a `Context` argument.
 - Allow for defaults and named arguments, using an `ExecOptions` struct
   that is passed as an argument.
 - Allow configuring command output handling in a flexible way. Instead
   of passing bools for some specific configurations, you can now pass
   either `Ignore`, `Show` or `Capture` for both stdout and stderr
   independently.
   By default, stdout is is shown when verbose is true, or ignored when
   verbose is false. Stderr is shown by default.
 - Actually redirect stdout to `/dev/null` when it is not needed (by
   leaving `command.Stdout` at nil). Previously, `ExecRecipe` would
   either show or capture stdout, and the captured output was usually
   just thrown away.

To allow for even more reuse, the biggest part of `ExecRecipe` is
extracted into a new `utils.ExecCommand()` function which executes an
arbitrary `exec.Cmd` object, with configurable output redirection.

Signed-off-by: Matthijs Kooijman <[email protected]>
Previously, the command was printed by PrepareCommandForRecipe. Letting
ExecCommand print seems more accurate, since it is only printed when it
is actually run (though this already happened in practice).
Additionally, the command can now be modified between
PrepareCommandForRecipe and ExecCommand while preserving correct output.

Since ExecCommand deals with a slice of arguments instead of a single
command string, this requires merging them together into a proper
commandline. Some care is taken to quote arguments containing
spaces, quotes or backslashes, though this is mostly intended for
display purposes. Arguments are only quoted when needed, regardless of
whether they were quoted in the original pattern.

Signed-off-by: Matthijs Kooijman <[email protected]>
Usually, the `preproc.macros` recipe includes the C/C++ flags, and
through that the `-MMD` flag to generate dependency files. However,
since include detection passed an output file of `/dev/null` (or the
equivalent on other operating systems), this causes gcc to try and
generate a `/dev/null.d` file and fail.

To prevent this, the `-MMD` flag was filtered out, but this filtering
was applied to the `compiler.cpp.flags` variable, where it *usually*
comes from. However, this is not necessarily true for all platforms. For
example, the PIC32 platform used to have this flag in the
`compiler.c.flags` variable and have `compiler.cpp.flags` include that.
This prevented the flag from being filtered away and caused a failure.

Due to previous changes, it is now possible for this filtering to happen
after all variables have been replaced and the command to run was
generated, but before actually running it. An extra advantage is that
the filtering is more robust than the previous substring-based
filtering.

This fixes arduino#230.

Signed-off-by: Matthijs Kooijman <[email protected]>
This slightly simplifies the code.

Signed-off-by: Matthijs Kooijman <[email protected]>
This does not use the passed variable yet, but prepares for a future
commit.

Signed-off-by: Matthijs Kooijman <[email protected]>
If -debug-level=20 is passed, whenever the cached file is not usable for
whatever reason, a message is displayed. This should help debug caching
problems.

The messages are hardcoded in the source and not put into `constants`,
since they are only debug messages.

Signed-off-by: Matthijs Kooijman <[email protected]>
This slightly cleans up a function call.

Signed-off-by: Matthijs Kooijman <[email protected]>
@matthijskooijman
Copy link
Collaborator Author

@facchinm @cmaglie, I just rebased this PR again, so it applies cleanly now. Could we perhaps merge this? Any issues were resolved, and since it touches quite some code, it is likely to run into new conflicts if we leave this unmerged.

@ArduinoBot
Copy link
Contributor

✅ Build completed.

⬇️ Build URL: http://downloads.arduino.cc/PR/arduino-builder/arduino-builder-236.zip

ℹ️ To test this build:

  1. Replace arduino-builder binary (you can find it where you installed the IDE) with the provided one

@cmaglie
Copy link
Member

cmaglie commented Jun 1, 2018

Everything has been manually merged.

@cmaglie cmaglie closed this Jun 1, 2018
@cmaglie cmaglie added this to the 1.3.26 milestone Jun 1, 2018
@matthijskooijman
Copy link
Collaborator Author

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants