Skip to content

Comments

Add -unittest=rootonly and -unittest=first flags#10937

Closed
alexandrumc wants to merge 8 commits intodlang:masterfrom
alexandrumc:r_uni
Closed

Add -unittest=rootonly and -unittest=first flags#10937
alexandrumc wants to merge 8 commits intodlang:masterfrom
alexandrumc:r_uni

Conversation

@alexandrumc
Copy link
Contributor

The default instantiation strategy (no -unittest) is to prefer the template instances that are instantiated in non-root modules in order to minimize the binary size and the compilation time (there are fewer symbols which require semantic analysis).

However, when the -unittest flag is enabled, the strategy is to prefer the template instances that are instantiated in root modules (this leads to larger binaries and compilation times but guarantees no link-time failures).

To avoid the compilation time and the binary size increase I propose two new flavors of the -unittest flag which use the default instantiation strategy and provide more flexibility in testing.

-unittest=rootonly enables only the version(unittest) {} and the unittest {} blocks that reside inside the root modules.

-unittest=first enables only the version(unittest) {} and the unittest {} blocks that reside in the first root module provided in the command line. This is useful if one wants to test only a single module but also has to provide other root modules for linking reasons.

@dlang-bot
Copy link
Contributor

Thanks for your pull request and interest in making D better, @alexandrumc! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please verify that your PR follows this checklist:

  • My PR is fully covered with tests (you can see the coverage diff by visiting the details link of the codecov check)
  • My PR is as minimal as possible (smaller, focused PRs are easier to review than big ones)
  • I have provided a detailed rationale explaining my changes
  • New or modified functions have Ddoc comments (with Params: and Returns:)

Please see CONTRIBUTING.md for more information.


If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment.

Bugzilla references

Your PR doesn't reference any Bugzilla issue.

If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog.

Testing this PR locally

If you don't have a local development environment setup, you can use Digger to test this PR:

dub run digger -- build "master + dmd#10937"

@WalterBright
Copy link
Member

To avoid the compilation time and the binary size increase

Do we really need to solve this problem? Adding more and more flags and conditions just makes things confusing. Unittest should be as simple as practical.

@Geod24
Copy link
Member

Geod24 commented Mar 18, 2020

Agreed with @WalterBright . I don't think exposing the complexities of template instantiation is a step forward.

@atilaneves
Copy link
Contributor

I think we need to fix this problem because one of D's strategic advantages are fast compile times, but they suffer immediately when -unittest is used.

@schveiguy
Copy link
Member

The issue with unittests is simpler IMO. We simply should not compile unittests in imported modules period. This is exactly how it works for non templated unittests. This is where the slowdown comes from.

See previous work here: #8124

@atilaneves
Copy link
Contributor

The issue with unittests is simpler IMO. We simply should not compile unittests in imported modules period. This is exactly how it works for non templated unittests. This is where the slowdown comes from.

That's not what @alexandrumc found.

@schveiguy
Copy link
Member

schveiguy commented Mar 19, 2020

That's not what @alexandrumc found.

What did he find? I found that non-templated unittests inside imports were not semantically analyzed. I had thought they were, but discovered it only happens for templates.

@ibuclaw
Copy link
Member

ibuclaw commented Mar 20, 2020

@schveiguy rereading the comment and taking it at face value. I understood it to be the following scenario.

Module A instantiates template FN in normal code, but compilation does not generate code for it because imported module B also instantiates the template with the same constraints.
If compiling with -unittest however, all modules generate code for FN. (I think I remember another command line switch that can invoke the same behaviour... -cov?).

Though I disagree that the above I described would increase binary size, as duplicates are discarded at link time.

There's a bug report somewhere that explains the historical why it is necessary to always generate code if unittests are being compiled too. However off the top of my head, I'd say it makes more sense to do this when -main is used as well.

@alexandrumc
Copy link
Contributor Author

If compiling with -unittest however, all modules generate code for FN. (I think I remember another command line switch that can invoke the same behaviour... -cov?).

Yes, that's exactly why it takes more time to compile.

Though I disagree that the above I described would increase binary size, as duplicates are discarded at link time.

Yeah, that's probably correct. It's my mistake. Now I realize that it's not the binary size which is increased but the obj/lib files. (e.g. when Phobos is compiled for unit testing, it is compiled one module at a time using -unittest flag and then the .o-s are bound together into a .lib; however, each object file was compiled with -unittest and has its own object code for its template instantiations, even if the same code/instances might be present in other modules; since there's no linker, the .lib will contain multiple TIs of the same type so its size will increase)

There's a bug report somewhere that explains the historical why it is necessary to always generate code if unittests are being compiled too.

I couldn't think of another reason than to avoid link-time errors.

@schveiguy
Copy link
Member

The problem that I've seen is that instantiated templates included their unittests even if they were imports. And it was really the semantic analysis that caused the slowdown. I'm trying to verify what actually happens with when I remember happening, but my various compiler installations are not all working.

There were cases where unittests were being semantically analyzed of imported templates, which in turn imported more stuff and then blew up into a huge problem. But I can't remember or re-prove what was happening. At this point, it appears that templates that are not instantiated locally do not include instantiated unittest semantic analysis, which is good. In the past it was included, but I don't know when that was stopped.

Imported templates that are instantiated locally still run unittests of those templates. I still think that should be stopped, there's no reason to test imported types that have documenting unittests just because you instantiated them locally.

@schveiguy
Copy link
Member

schveiguy commented Mar 20, 2020

OK, so the analysis is still done in certain cases. So for example:

module mod1;

struct S()
{
  unittest { pragma(msg, "analyzed"); }
}
version(test1) alias sinst = S!();
version(test2) void foo(S!() s) {}

And the importing module:

import mod1;
version(test3) alias sinst = S!();
void main() {}

So let's compile mod1 like this:

dmd -c mod1.d

No instantiations, no unittests.

What happens when you compile main.d?

test case Compilation output Runtime output Unittest Analyzed Unittest Included
dmd -unittest -version=test1 main.d mod1.o (nothing) (nothing) No No
dmd -unittest -version=test2 main.d mod1.o analyzed (nothing) Yes No
dmd -unittest -version=test3 main.d mod1.o analyzed 1 Unittests passed Yes Yes

So IMO, the most damaging one is test2. You are semantically analyzing the unittest, which means importing and compiling all it requires without actually including the unittest.

test3 is also not ideal, I don't think there's any reason to run unittests of imported modules period.

@alexandrumc
Copy link
Contributor Author

OK, so the analysis is still done in certain cases. So for example:

module mod1;

struct S()
{
  unittest { pragma(msg, "analyzed"); }
}
version(test1) alias sinst = S!();
version(test2) void foo(S!() s) {}

And the importing module:

import mod1;
version(test3) alias sinst = S!();
void main() {}

So let's compile mod1 like this:

dmd -c mod1.d

No instantiations, no unittests.

What happens when you compile main.d?

test case Compilation output Runtime output Unittest Analyzed Unittest Included
dmd -unittest -version=test1 main.d mod1.o (nothing) (nothing) No No
dmd -unittest -version=test2 main.d mod1.o analyzed (nothing) Yes No
dmd -unittest -version=test3 main.d mod1.o analyzed 1 Unittests passed Yes Yes
So IMO, the most damaging one is test2. You are semantically analyzing the unittest, which means importing and compiling all it requires without actually including the unittest.

test3 is also not ideal, I don't think there's any reason to run unittests of imported modules period.

Thanks for the detailed explanation. I've compiled (the same way you did) using the changes in this PR (with -unittest=rootonly and with -unittest=first) and the unittest is not analyzed or included in any of the 3 cases. That's because the unittests in non-root modules are not even parsed. Could you please try it too?

@schveiguy
Copy link
Member

That's because the unittests in non-root modules are not even parsed.

Yes, but I'm saying that should be the way it ALWAYS is, not just behind a switch.

@schveiguy
Copy link
Member

I suppose I should clarify a bit. Yes, I agree that only root unittests should be compiled. In response to Walter, that is the simplest possible solution. I can't seen any use case to compile unit tests in imported modules that aren't being included as part of the compilation unit.

I understand that this PR does this for a specialized switch -- I think it should be always. I don't think the first option needs to exist, you can already separately compile files with different switches.

And I don't buy that the emitting of the compiled code to object files is the problem, I think it's the semantic analysis. This PR solves both, but I think a simpler solution could solve the most expensive one.

@schveiguy
Copy link
Member

schveiguy commented Mar 20, 2020

-unittest=rootonly enables only the version(unittest) {} and the unittest {} blocks that reside inside the root modules.

BTW, version(unittest) should NOT be turned off if -unittest or -unittest=anything is on. It's too confusing/disruptive to do this.

@schveiguy
Copy link
Member

This PR solves both, but I think a simpler solution could solve the most expensive one.

I guess you solve the semantic analysis you solve both :) So yeah, my complaint really just falls back on, why don't we always do what -unittest=rootonly does in this PR.

@ibuclaw
Copy link
Member

ibuclaw commented Mar 20, 2020

Let's see what the testsuite says first before making it the default.

@alexandrumc
Copy link
Contributor Author

-unittest=rootonly enables only the version(unittest) {} and the unittest {} blocks that reside inside the root modules.

BTW, version(unittest) should NOT be turned off if -unittest or -unittest=anything is on. It's too confusing/disruptive to do this.

version (unittest) usually covers top-level templates. If the version (unittest) blocks not turned off, the semantic analysis will have much more work to do, so the compile-time will increase.

@Geod24
Copy link
Member

Geod24 commented Mar 24, 2020

the semantic analysis will have much more work to do

Only if those templates are instantiated, no ?

@alexandrumc
Copy link
Contributor Author

the semantic analysis will have much more work to do

Only if those templates are instantiated, no ?

Yes, but that's not so uncommon to see. This is why I changed all the version (unittest)s from Phobos and DRuntime to version (StdUnittest) and version (CoreUnittest). I'm sure no one wants to have hundreds of symbols analyzed (used for unit testing the standard library and the runtime) when he tests his own project.

@alexandrumc
Copy link
Contributor Author

Let's see what the testsuite says first before making it the default.

I've changed the -unittest=rootonly to default. As expected, everything fails because templates are no longer instantiated in root modules if the same types of instantiations are already present in non-root modules. So in my opinion, the issue is not the functionality of the flag, it's the way projects are compiled for unit testing.

This is why I think this flag shouldn't directly replace -unittest. I think that at some point -unittest should be deprecated and be completely replaced by -unittest=rootonly, but that only if the users change the way they do unit testing for their projects (if they are interested in faster compilation times). Maybe that won't ever happen, but I still think there are some people interested in this kind of behavior and this new flavor would help them.

@Geod24
Copy link
Member

Geod24 commented Mar 24, 2020

Those failures seems to indicate another problem. Just looking at two random failures:

sh: line 1: 18715 Illegal instruction     (core dumped) generated/runnable/issue16995_4 > generated/92f0d3bd10dd5a60e69655bfd0ee7a981baf50fa83f09b08cfbb8f525a96a11 2>&1
 ... runnable/issue16995.d          -unittest  -fPIC (-O -inline -release)
Test runnable/issue16995.d failed.  The logged output:
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC  -odgenerated/runnable -c  runnable/issue16995.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC  -odgenerated/runnable -c  runnable/imports/module_with_tests.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC  -odgenerated/runnable -c  runnable/imports/another_module_with_tests.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64  -fPIC  -odgenerated/runnable -ofgenerated/runnable/issue16995_0 generated/runnable/issue16995.o generated/runnable/module_with_tests.o generated/runnable/another_module_with_tests.o
generated/runnable/issue16995_0

/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -O -odgenerated/runnable -c  runnable/issue16995.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -O -odgenerated/runnable -c  runnable/imports/module_with_tests.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -O -odgenerated/runnable -c  runnable/imports/another_module_with_tests.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64  -fPIC  -odgenerated/runnable -ofgenerated/runnable/issue16995_1 generated/runnable/issue16995.o generated/runnable/module_with_tests.o generated/runnable/another_module_with_tests.o
generated/runnable/issue16995_1

/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -inline -odgenerated/runnable -c  runnable/issue16995.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -inline -odgenerated/runnable -c  runnable/imports/module_with_tests.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -inline -odgenerated/runnable -c  runnable/imports/another_module_with_tests.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64  -fPIC  -odgenerated/runnable -ofgenerated/runnable/issue16995_2 generated/runnable/issue16995.o generated/runnable/module_with_tests.o generated/runnable/another_module_with_tests.o
generated/runnable/issue16995_2

/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -O -inline -odgenerated/runnable -c  runnable/issue16995.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -O -inline -odgenerated/runnable -c  runnable/imports/module_with_tests.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -O -inline -odgenerated/runnable -c  runnable/imports/another_module_with_tests.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64  -fPIC  -odgenerated/runnable -ofgenerated/runnable/issue16995_3 generated/runnable/issue16995.o generated/runnable/module_with_tests.o generated/runnable/another_module_with_tests.o
generated/runnable/issue16995_3

/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -release -odgenerated/runnable -c  runnable/issue16995.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -release -odgenerated/runnable -c  runnable/imports/module_with_tests.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC -release -odgenerated/runnable -c  runnable/imports/another_module_with_tests.d
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64  -fPIC  -odgenerated/runnable -ofgenerated/runnable/issue16995_4 generated/runnable/issue16995.o generated/runnable/module_with_tests.o generated/runnable/another_module_with_tests.o
generated/runnable/issue16995_4


==============================
Test runnable/issue16995.d failed: expected rc == 0, exited with rc == 132

This test does not use templates:

 ... runnable/test11745.d           -unittest  -fPIC ()
Test runnable/test11745.d failed.  The logged output:
/media/disk1/braddr/sandbox/at-client/pull-3987706-Linux_64_64/dmd/generated/linux/release/64/dmd -conf= -m64 -Irunnable -unittest  -fPIC  -odgenerated/runnable -ofgenerated/runnable/test11745_0  runnable/test11745.d runnable/imports/test11745b.d 
generated/runnable/test11745_0.o: In function `_Dmain':
runnable/imports/test11745b.d:(.text._Dmain[_Dmain]+0xa): undefined reference to `_D7imports10test11745b16__unittest_L8_C9FZv'
runnable/imports/test11745b.d:(.text._Dmain[_Dmain]+0xf): undefined reference to `_D7imports10test11745b17__unittest_L14_C1FZv'
collect2: error: ld returned 1 exit status
Error: linker exited with status 1


==============================
Test runnable/test11745.d failed: expected rc == 0, exited with rc == 1

Likewise, this test doesn't involve templates:

@Geod24
Copy link
Member

Geod24 commented Mar 24, 2020

I'm sure no one wants to have hundreds of symbols analyzed (used for unit testing the standard library and the runtime) when he tests his own project.

You'd be surprised.

@schveiguy
Copy link
Member

version (unittest) usually covers top-level templates. If the version (unittest) blocks not turned off, the semantic analysis will have much more work to do, so the compile-time will increase.

Yes, and that's on the library developer to design properly. Note that a library author could include unittesting facilities for their library in case you want to integration test your project with their library. Or it might be something that's contained into one module for use in testing several modules, and only imported inside unittests.

There are definitely ways to use version(unittest) that don't impact importing modules, and there are ways to do it badly. But we should not restrict the feature, especially when there are valid uses.

@alexandrumc alexandrumc requested a review from RazvanN7 as a code owner March 26, 2020 16:43
@alexandrumc alexandrumc reopened this Mar 30, 2020
@alexandrumc alexandrumc closed this Apr 1, 2020
@alexandrumc alexandrumc reopened this Apr 1, 2020
@RazvanN7
Copy link
Contributor

This seems to be outdated, given the recent template emission fixes.

@RazvanN7 RazvanN7 closed this Mar 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants