Open aisamu opened 7 years ago
Interesting observations. But I'm sorry I'm not going to investigate it.
Currently each release of cljs-devtools performs following dead-code elimination sanity check:
1) compiles a trivial empty project with :dependencies
, :require
and goog.DEBUG
set to false
under :advanced
mode with :pseudo-names
: https://github.com/binaryage/cljs-devtools/blob/ded5a93adfa0215a1b1bf64f0b22212fa3e2ad8a/project.clj#L73-L82
2) uses this script to check that no traces of devtools namespace are present: https://github.com/binaryage/cljs-devtools/blob/master/test/scripts/dead-code-check.sh
It seems to work in this case. But I don't check files sizes or how it affects non-devtools code. One possible theory could be that although devtools namespaces are not present, still requiring them triggers Google Closure to compile-in more of the standard library. For example whole cljs printing machinery (println
and friends) which would be normally dead-code eliminated.
Please investigate it further and let me know. You might also want to test more recent ClojureScript versions because this might be dependent on Google Closure compiler version.
Nice catch! Unfortunately, updating Clojurescript to 1.9.494
made things a bit worse.
There seems to be no difference between goog.DEBUG (Typo on the scrips!)true
or false
versions. (I re-checked it three times over).
Here are the updated sizes:
Type clojurescript "1.9.494" |
Size |
---|---|
:dependencies + :require + goog.DEBUG true |
2.1 MB |
:dependencies + :require + goog.DEBUG false |
1.7 MB |
:dependencies + :require + no mention at all |
1.7 MB |
:dependencies + no :require + no mention at all |
860 KB |
no :dependencies + no :require + no mention at all |
860 KB |
This is way over my league, but I'll gladly follow your lead if you have more ideas.
Regarding your DCE sanity check:
:pseudo-names
from!$> cat devtools-dce-pseudo-name-inline-debug-debug-true/resources/public/js/compiled/app.js | perl -pe 's/(\\$|\\d+)\\$/\\1\\$\\n/g' | grep -o 'devtools\\$.*'
(no output)
$> sw_vers
ProductName: Mac OS X
ProductVersion: 10.10.5
BuildVersion: 14F2315
$> perl --version
This is perl 5, version 18, subversion 2 (v5.18.2) built for darwin-thread-multi-2level
$> grep --version
grep (BSD grep) 2.5.1-FreeBSD
I was curious. I have just added some helper scripts to compare DCE in scenarios you described.
You can run lein compare-dead-code
or lein compare-dead-code-with-pseudo-names
.
My output looks like this:
> lein compare-dead-code
Compiling ClojureScript...
Compiling "test/resources/.compiled/dce-no-debug/build.js" from ["src/lib" "test/src/dead-code"]...
Successfully compiled "test/resources/.compiled/dce-no-debug/build.js" in 15.833 seconds.
Compiling ClojureScript...
Compiling "test/resources/.compiled/dce-with-debug/build.js" from ["src/lib" "test/src/dead-code"]...
Successfully compiled "test/resources/.compiled/dce-with-debug/build.js" in 15.853 seconds.
Compiling ClojureScript...
Compiling "test/resources/.compiled/dce-no-mention/build.js" from ["src/lib" "test/src/dead-code-no-mention"]...
Successfully compiled "test/resources/.compiled/dce-no-mention/build.js" in 15.609 seconds.
Compiling ClojureScript...
Compiling "test/resources/.compiled/dce-no-require/build.js" from ["src/lib" "test/src/dead-code-no-require"]...
Successfully compiled "test/resources/.compiled/dce-no-require/build.js" in 8.852 seconds.
Compiling ClojureScript...
Compiling "test/resources/.compiled/dce-no-sources/build.js" from ["test/src/dead-code-no-require"]...
Successfully compiled "test/resources/.compiled/dce-no-sources/build.js" in 9.004 seconds.
stats:
WITH_DEBUG: 368048 bytes
NO_DEBUG: 297414 bytes
NO_MENTION: 297414 bytes
NO_REQUIRE: 4778 bytes
NO_SOURCES: 4778 bytes
beautified with-debug.js
beautified no-debug.js
beautified no-mention.js
beautified no-require.js
beautified no-sources.js
beautified sources in test/resources/.compiled/dead-code-compare
see https://github.com/binaryage/cljs-devtools/issues/37
These results are consistent with my theory:
:closure-defines {goog.DEBUG false}
worksDuring writing these scripts I got completely puzzled by behaviour of lein/cljsbuild/cljs-compiler. It looks like cljs compiler incorrectly reuses caches between individual cljsbuild builds. That caused that my results were illogical and depended on the order of compilation of individual cljsbuild builds and cljs compiler cache state. I had to brute-force lein clean
between individual dce builds just to be sure I get consistent results. It seems to me that you hit similar issue with your measurements because you should be able to reproduce same file size relationships.
Oh, there was a typo on the scripts that I managed to overlook three times in a row... I've corrected the second row above. The results went back to the previous distribution, just a bit larger.
Yup, I had already been bitten by this cljsbuild behavior, it's almost a "standard". As a matter of fact, I ran each of those tests on a brand-new, separate project.
I've just tried the whole procedure with reagent
and got similar results. It looks like I was expecting too much from dead code elimination, and the way to go for complete elision is indeed different :source-paths
.
Thanks for the attention (and for cljs-devtools!)
The results for reagent
:
[clojurescript "1.9.494"] , [reagent "0.6.0"] |
Size |
---|---|
:dependencies + :require + goog.DEBUG true |
844 KB |
:dependencies + :require + goog.DEBUG false |
838 KB |
:dependencies + :require + no mention at all |
839 KB |
:dependencies + no :require + no mention at all |
425 KB |
no :dependencies + no :require + no mention at all |
425 KB |
I dug deeper into this and I have some bad news.
So far I identified following problems:
1) I used to think that def-ing static config maps[1] is DCE-friendly. Unfortunately it turns out that if the map has more than 8 keys, the cljs compiler emits cljs.core.PersistentHashMap.fromArrays
call which ruins DCE. [my theory] Probably because internally it uses transients which use protocols which are too dynamic. Closure Compiler then gets convinced to compile-in large part of cljs.core library.
2) requiring cljs.pprint
without ever using any of its functionality also triggers huge compilation output alone. I briefly investigated its generated javascript before advanced mode optimization and it definitely suffers from 1) as well (specifically cljs.pprint.*code-table*
and cljs.pprint.directive-table
)
3) just requiring cljs.stacktrace
also triggers huge compilation output alone. There are no static cljs.core.PersistentHashMap.fromArrays
calls. But it declares multi-methods which is probably the cause of code bloat.
Please note that cljs.pprint
also uses multi-methods so both issues might trigger large compilation output there.
I don't have a good solution for this right now. I was able to work-around cljs.core.PersistentHashMap.fromArrays
issue by wrapping static def-s in dynamic functions which get called lazily on first-usage. This fixed the issue in cljs-devtools code. But I'm not able to easily fix cljs.pprint
and cljs.stacktrace
.
Ok, I was able to shave off quite some dead weight: https://travis-ci.org/binaryage/cljs-devtools/builds/221500541#L333-L337
The small difference between NO_DEBUG and NO_REQUIRE is caused by requiring goog.userAgent
. So even google closure library authors do not always produce code which could be fully understood by DCE in closure compiler. I could eventually apply the same trick as with cljs.pprint and cljs.stacktrace, but IMO it is not worth it ATM.
Wow, that's really great! It effectively brought DCE back as a viable option.
Following today's Clojurescript release (with the accompanying Google Closure version bump), here are the new results:
[clojurescript "1.9.518"] , [binaryage/devtools "0.9.3-SNAPSHOT"] * |
Size |
---|---|
:dependencies + :require + goog.DEBUG true |
1.3 MB |
:dependencies + :require + goog.DEBUG false |
844 KB |
:dependencies + :require + no mention at all |
844 KB |
:dependencies + no :require + no mention at all |
841 KB |
no :dependencies + no :require + no mention at all |
841 KB |
*https://github.com/binaryage/cljs-devtools/commit/527072690f67db17cbedec943f3c1b1a3b575d27
Your version of "I'm sorry I'm not going to investigate it" is putting our's "I'm 110% on it" to shame.
Thank you!
Wait, I was too fast. It didn't work as expected.
Unfortunately when the library is packaged as maven dependency the optional requires didn't work.
At least I implemented a compile-time warning. The solution is to use :preloads
compiler option or different sets of :source-paths
in advanced mode to exclude traces of cljs-devtools from the build completely.
@aisamu Just to add... Your sizes of compiled files are consistent and what I would expect with https://github.com/binaryage/cljs-devtools/commit/527072690f67db17cbedec943f3c1b1a3b575d27. You would just see a runtime error when running a dev build of your app. It would complain that "goog.require could not find: devtools.optional".
Oh, sad to hear.
Perhaps it'd be useful to other library/tool writers if you post something on Clojurescript's mailing list. At the very least you'll have more experienced eyes on the matter.
I've collected some threads below related to DCE, but I'm not really qualified to assess whether you've stumbled on something new/different or not, so I'll leave them here for your appraisal:
:require
inflates size
https://groups.google.com/forum/#!searchin/clojurescript/dead$20code|sort:relevance/clojurescript/l6_JekFUu6g/F8UiuR08sBYJ
Top level data structures (as you mentioned) https://groups.google.com/forum/#!searchin/clojurescript/dead$20code|sort:relevance/clojurescript/3QmukS-q9kw/8Tyo0nd8-X4J
Multimethods (as you mentioned as well) https://groups.google.com/forum/#!searchin/clojurescript/dead$20code|sort:relevance/clojurescript/3rL8vKzyJQA/w9_XwpGtBAAJ
I can't seem to bring the code size down with a barely empty project through dead-code elimination alone, even when following the installation instructions to the letter.
The template used was reagent-figwheel with only
devtools
andreagent
as dependencies. I've fiddled with thegoog.DEBUG
flag, and then removeddevtools
from the:require
and:dependencies
vectors.:dependencies
+:require
+goog.DEBUG true
:dependencies
+:require
+goog.DEBUG false
:dependencies
+:require
+ no mention at all:dependencies
+ no:require
+ no mention at all:dependencies
+ no:require
+ no mention at allThe first two rows followed the instructions from the release notes. There's a single mention of
(devtools/install!)
within a(when ^boolean js/goog.DEBUG ...)
block. The other three had this line removed manually.Google Closure's shaved 400kb of the build, but that's still a 800kb increase for a
:require
without a single mention ofdevtools
! Is this the best I can expect from dead code elimination?Here's the code used for the builds (e.g.
lein new reagent-figwheel +devtools
):