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

Some X.A targets do not respect Inputs/Outputs timestamp invariant #2247

Closed
garuma opened this issue Oct 1, 2018 · 5 comments
Closed

Some X.A targets do not respect Inputs/Outputs timestamp invariant #2247

garuma opened this issue Oct 1, 2018 · 5 comments
Assignees

Comments

@garuma
Copy link
Contributor

garuma commented Oct 1, 2018

I'm debugging an incremental compilation for a project of ours and I have identified at least 3 spots where X.A targets are using CopyIfChanged or a variant-thereof and thus breaking inputs/outputs invariant.

What I mean by this is that you have plenty of scenarios where some target Inputs are newer than Outputs causing the target to run yet those updated Inputs will end up generating the same Outputs content. Because of the logic of CopyIfChanged, this mean that no actual change is committed to Outputs and thus when the target finishes executing you have broken the invariant that Outputs always have to be newer that your Inputs.

Three places where I have seen this (there might be more):

  • _CopyIntermediateAssemblies (Outputs are also wrong for this one, they lack the satellites copy)
  • _GeneratePackageManagerJava
  • _ResolveLibraryProjectImports
@garuma
Copy link
Contributor Author

garuma commented Oct 1, 2018

Thinking about it more, I don’t know if there is any way you can salvage that CopyIfChanged logic, it just doesn't play well with MBuild.

Maybe what can be done is that the heavy targets output an intermediary file that’s always written to and then a second target copy that intermediate file to the real location if it has changed. That way the heavy task will respect incrementality and not be re-run all the time while the CopyIfChanged based target will be the one that runs all the time but at least it won’t cost anything

@grendello grendello added this to the far future milestone Oct 2, 2018
@jonathanpeppers
Copy link
Member

@garuma if these targets are running all the time, and taking > 0ms, we should look into this.

The immediate idea that comes to mind is to use a "stamp" file:

<Target Name="_ResolveLibraryProjectImports" Inputs="..." Outputs="$(IntermediateOutputPath)libraryprojectimports.stamp">
  <!--Whatever the target does-->
  <Touch Files="$(IntermediateOutputPath)libraryprojectimports.stamp" AlwaysCreate="True" />
  <ItemGroup>
    <FileWrites Include="$(IntermediateOutputPath)libraryprojectimports.stamp" />
  </ItemGroup>
</Target

We would also need to verify the inputs are correct.

@garuma is there an easy way I can reproduce this? or can you attach binlogs?

@jonathanpeppers jonathanpeppers self-assigned this Oct 12, 2018
@garuma
Copy link
Contributor Author

garuma commented Oct 12, 2018

@jonathanpeppers yeah making everything do stamps might be the easiest things considering it's already done throughout.

I had started writing a skeleton of unit-test showing the issue but basically the gist of it is to start touch-ing random input files (e.g. built dlls, NuGet dlls, referenced binding projects stuff...) and execute MSBuild multiple time to see targets being constantly re-run.

@jonathanpeppers
Copy link
Member

jonathanpeppers commented Oct 16, 2018

@garuma I can reproduce this issue for _CopyIntermediateAssemblies with the following test:

[Test]
public void CopyIntermediateAssemblies ()
{
	var target = "_CopyIntermediateAssemblies";
	var proj = new XamarinFormsAndroidApplicationProject ();
	using (var b = CreateApkBuilder (Path.Combine ("temp", TestName))) {
		Assert.IsTrue (b.Build (proj), "first build should succeed");
		Assert.IsFalse (b.Output.IsTargetSkipped (target), $"`{target}` should *not* be skipped!");

		var assembly = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, proj.ProjectName + ".dll");
		FileAssert.Exists (assembly);
		File.SetLastWriteTimeUtc (assembly, DateTime.UtcNow);
		File.SetLastAccessTimeUtc (assembly, DateTime.UtcNow);

		//NOTE: second build, target will run because inputs changed
		Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "second build should succeed");
		Assert.IsFalse (b.Output.IsTargetSkipped (target), $"`{target}` should *not* be skipped on second build!");

		//NOTE: third build, it should certainly *not* run! there are no changes
		Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "third build should succeed");
		Assert.IsTrue (b.Output.IsTargetSkipped (target), $"`{target}` should be skipped on third build!");
	}
}

This is going to certainly impact incremental build times... I will probably investigate each target separately and create a PR for each.

@garuma
Copy link
Contributor Author

garuma commented Oct 16, 2018

For reference, we also logged an enhancement in MSBuild itself to potentially surface those type of issues more easily: dotnet/msbuild#3817

jonathanpeppers added a commit to jonathanpeppers/xamarin-android that referenced this issue Oct 16, 2018
Context: dotnet#2247
Context: dotnet#2128

The `_CopyIntermediateAssemblies` target has a bug for incremental
builds, reproduced by the following:
- Build a Xamarin.Android app project
- `touch` the project's assembly in `$(IntermediateOutputPath)`
- Build a second time. `_CopyIntermediateAssemblies` will run as
  expected.
- Build a third time. `_CopyIntermediateAssemblies` will still run!

When in this state, `_CopyIntermediateAssemblies` will always run.
This is bad because it triggers other expensive targets like
`_UpdateAndroidResgen` _every time_.

I believe I introduced this in dotnet#2128, which was when I saw
`_CopyIntermediateAssemblies` taking more time than it used to. It was
a good tradeoff though, since it prevented `_UpdateAndroidResgen` from
running too often. 15.9 does not have dotnet#2128.

To fix this problem properly, I introduced a new
`_copyintermediate.stamp` file to be used as the `Outputs` of the
target. In addition to fixing our incremental build here, there should
be performance gains in only verifying the timestamp of one file in
`Outputs`.

The `Inputs` of the `_CopyIntermediateAssemblies` were also incorrect,
as it was not using the "satellite" assembly files as an input.

This does not fully complete dotnet#2247, as there are two other targets
listed I need to investigate further. These are likely unrelated to
the changes in dotnet#2128.
jonpryor pushed a commit that referenced this issue Oct 17, 2018
…#2308)

Context: #2247
Context: c2d3681

The `_CopyIntermediateAssemblies` target has a bug for incremental
builds, reproduced by the following:

 1. Build a Xamarin.Android app project
 2. `touch` the project's assembly in `$(IntermediateOutputPath)`
 3. Build a second time. `_CopyIntermediateAssemblies` will run as
    expected.
 4. Build a third time. `_CopyIntermediateAssemblies` still runs!

When in this state, `_CopyIntermediateAssemblies` will always run.
This is bad because it triggers other expensive targets like
`_UpdateAndroidResgen` *every time*.

I believe I introduced this in c2d3681, which was when I saw
`_CopyIntermediateAssemblies` taking more time than it used to.
It was a good tradeoff though, since it prevented the
`_UpdateAndroidResgen` target from running too often.

d15-9 does not have c2d3681.

To fix this problem properly, I introduced a new
`_copyintermediate.stamp` file to be used as the `Outputs` of the
target.  In addition to fixing our incremental build here, there
should be performance gains in only verifying the timestamp of one
file in `Outputs`.

The `Inputs` of the `_CopyIntermediateAssemblies` were also
incorrect, as it was not using the "satellite" assembly files as an
input.

This does not fully finish #2247, as there are two other targets
listed I need to investigate further.  These are likely unrelated to
the changes in c2d3681.
jonathanpeppers added a commit to jonathanpeppers/xamarin-android that referenced this issue Oct 18, 2018
Fixes: dotnet#2247

We currently have two targets that fail to build incrementally:
- `_ResolveLibraryProjectImports`
- `_GeneratePackageManagerJava`

Both of these targets declare `Outputs`, but don't always update the
timestamp of the output file. They used `CopyIfChanged` logic, so they
won't write to the file if nothing changed.

However...

We can't just add `<Touch/>` calls here either, as the `Outputs` of
these targets are `Inputs` to other *slow* targets (`_CompileJava`,
`_UpdateAndroidResgen`, and friends). If we used `<Touch/>` here,
things would be worse and trigger the *slow* targets to run.

So the only option here is to use a "stamp" file, this allows these
targets to properly build incrementally and not trigger other targets.

I updated a `IncrementalBuildTest` to include a test case checking all
three of these targets.

Future changes:

- I am going to need to audit many of our MSBuild targets for this
  same problem, and so I will likely need to add many new stamp files.
- To simplify this, we should adopt a new convention of placing stamp
  files in `$(IntermediateOutputPath)stamp\` or
  `$(_AndroidStampDirectory)`.
- The stamp file should be named the same as the target where it is
  used as an `Output`.

Documentation:

I also started some initial documentation on "MSBuild Best Practices",
which we can expand upon in future PRs. See the `Stamp Files` section
on what is relevant in this PR.
dellis1972 pushed a commit that referenced this issue Oct 19, 2018
Fixes: #2247

We currently have two targets that fail to build incrementally:
- `_ResolveLibraryProjectImports`
- `_GeneratePackageManagerJava`

Both of these targets declare `Outputs`, but don't always update the
timestamp of the output file. They used `CopyIfChanged` logic, so they
won't write to the file if nothing changed.

However...

We can't just add `<Touch/>` calls here either, as the `Outputs` of
these targets are `Inputs` to other *slow* targets (`_CompileJava`,
`_UpdateAndroidResgen`, and friends). If we used `<Touch/>` here,
things would be worse and trigger the *slow* targets to run.

So the only option here is to use a "stamp" file, this allows these
targets to properly build incrementally and not trigger other targets.

I updated a `IncrementalBuildTest` to include a test case checking all
three of these targets.

Future changes:

- I am going to need to audit many of our MSBuild targets for this
  same problem, and so I will likely need to add many new stamp files.
- To simplify this, we should adopt a new convention of placing stamp
  files in `$(IntermediateOutputPath)stamp\` or
  `$(_AndroidStampDirectory)`.
- The stamp file should be named the same as the target where it is
  used as an `Output`.

Documentation:

I also started some initial documentation on "MSBuild Best Practices",
which we can expand upon in future PRs. See the `Stamp Files` section
on what is relevant in this PR.
@ghost ghost locked as resolved and limited conversation to collaborators Jun 7, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants