Skip to content

Loading…

feat #329 Remove unnecessary code during Grunt build: part1 #330

Closed
wants to merge 1 commit into from

2 participants

@jakub-g
Aria Templates member

This commits brings a general-usage function to do recursive replacements in the AST given the replacements conditions, and adds two sample visitors that remove [$]descriptions from the beans and event definitions.

The visitors are also capable to compute the number of bytes saved in the process (the number is displayed at the end of the build by a newly created Grunt task).

@jakub-g
Aria Templates member

Output of 1.3.4 build: https://travis-ci.org/ariatemplates/ariatemplates/builds/4091411/#L1050
Output of 1.3.4 build + this commit on top: https://travis-ci.org/jakub-g/ariatemplates/builds/4144001/#L1030
About 100 kB of minified code (30 kB of minified+gzipped code) are saved.

@jakub-g
Aria Templates member

All the nodes removed are logged in verbose mode of Grunt; to inspect them, one can run for instance this command:

grunt --verbose > grunt.log; less grunt.log

or perhaps instead of less:

cat grunt.log | grep "[min]" (note that due to Node/Grunt bug, you can't grep the Grunt's output directly).

@jakub-g jakub-g commented on an outdated diff
build/grunt-tasks/grunt-min-override.js
((56 lines not shown))
+ var pro = uglifyjs.uglify;
+ var ast, pos;
+ var msg = 'Minifying with UglifyJS...';
+
+ grunt.verbose.write(msg);
+ try {
+ ast = jsp.parse(src);
+
+ // == ARIA TEMPLATES OVERRIDE START: unnecessary code removal ==
+ var savedBytes = grunt.helper('removeUnnecessaryCode', ast);
+ if(savedBytes){
+ grunt.helper('uselessCodeSavedBytes', savedBytes);
+ grunt.verbose.writeln("[min] Useless code removal: saved " + savedBytes + " bytes.");
+ grunt.verbose.writeln();
+ }
+ // == ARIA TEMPLATES OVERRIDE END ==
@jakub-g Aria Templates member
jakub-g added a note

This file has been copied from Grunt source code, except lines 64-71 which are our override (I modified the original file to make use of existing AST [line 62] instead of having to recalculate it in a separate task).

@jakub-g Aria Templates member
jakub-g added a note

Perhaps we can redefine only the helper, not the whole file -- to be checked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@divdavem divdavem was assigned
@jakub-g jakub-g feat 329 Remove unnecessary code during Grunt build: part1
This commits brings a general-usage function to do recursive replacements
in the AST given the replacements conditions, and adds two sample visitors
that remove [$]descriptions from the beans and event definitions.

The visitors are also capable to compute the number of bytes saved in the
process (the number is displayed at the end of the build by a newly
created Grunt task).
1387ee7
@divdavem
Aria Templates member

Integrated in 6c1c4f7

@divdavem divdavem closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 15, 2013
  1. @jakub-g

    feat 329 Remove unnecessary code during Grunt build: part1

    jakub-g committed
    This commits brings a general-usage function to do recursive replacements
    in the AST given the replacements conditions, and adds two sample visitors
    that remove [$]descriptions from the beans and event definitions.
    
    The visitors are also capable to compute the number of bytes saved in the
    process (the number is displayed at the end of the build by a newly
    created Grunt task).
View
4 build/build-os-prod.js
@@ -237,10 +237,10 @@ module.exports = function(grunt) {
grunt.registerTask('buildProdFromTmp', 'removedirs:prod copyProd atmapreader packager verifypackager md5 atmapwriter');
grunt.registerTask('buildProdFromTmpNoVersion', 'removedirs:prod copyProd atmapreader packager verifypackager atmapwriter');
grunt.registerTask('release', 'buildTmp buildProdFromTmp');
- grunt.registerTask('releaseAndStats', 'release gzipStats');
+ grunt.registerTask('releaseAndStats', 'release removedCodeStats gzipStats');
grunt.registerTask('releaseAndClean', 'release postCleanup');
grunt.registerTask('releaseAndCleanNoVersion', 'buildTmp buildProdFromTmpNoVersion postCleanup');
- grunt.registerTask('releaseAndStatsAndClean', 'release gzipStats postCleanup');
+ grunt.registerTask('releaseAndStatsAndClean', 'release removedCodeStats gzipStats postCleanup');
//grunt.registerTask('default', 'buildProdFromTmp'); // for debugging
//grunt.registerTask('default', 'gzipStats'); // for debugging
View
66 build/grunt-tasks/helpers-atuglify.js
@@ -1,17 +1,67 @@
-/**
- * Wrapper for grunt's 'uglify' task to add the possibility to split long lines.
+/*
+ * grunt
+ * http://gruntjs.com/
+ *
+ * Copyright (c) 2012 "Cowboy" Ben Alman
+ * Licensed under the MIT license.
+ * https://github.com/gruntjs/grunt/blob/master/LICENSE-MIT
*/
-module.exports = function(grunt) {
+/*
+ * The code is Grunt 0.3.17 'uglify' helper code with small Aria Templates overrides.
+ */
+module.exports = function (grunt) {
+
+ // External libs.
var uglifyjs = require('uglify-js');
- grunt.registerHelper('atuglify', function(src, options) {
- src = grunt.helper('uglify', src, options);
- src = grunt.helper('uglifySplitLines', src, options.max_line_length);
- return src;
+ // Minify with UglifyJS.
+ // From https://github.com/mishoo/UglifyJS
+ grunt.registerHelper('atuglify', function (src, options) {
+ if (!options) {
+ options = {};
+ }
+ var jsp = uglifyjs.parser;
+ var pro = uglifyjs.uglify;
+ var ast, pos;
+ var msg = 'Minifying with UglifyJS...';
+
+ grunt.verbose.write(msg);
+ try {
+ ast = jsp.parse(src);
+
+ // == ARIA TEMPLATES OVERRIDE START: unnecessary code removal ==
+ ast = grunt.helper('removeUnnecessaryCode', ast);
+ // == ARIA TEMPLATES OVERRIDE END ==
+
+ ast = pro.ast_mangle(ast, options.mangle || {});
+ ast = pro.ast_squeeze(ast, options.squeeze || {});
+ src = pro.gen_code(ast, options.codegen || {});
+
+ // == ARIA TEMPLATES OVERRIDE START: split long lines ==
+ src = grunt.helper('uglifySplitLines', src, options.max_line_length);
+ // == ARIA TEMPLATES OVERRIDE END ==
+
+ // Success!
+ grunt.verbose.ok();
+ // UglifyJS adds a trailing semicolon only when run as a binary.
+ // So we manually add the trailing semicolon when using it as a module.
+ // https://github.com/mishoo/UglifyJS/issues/126
+ return src + ';';
+ } catch (e) {
+ // Something went wrong.
+ grunt.verbose.or.write(msg);
+ pos = '['.red + ('L' + e.line).yellow + ':'.red + ('C' + e.col).yellow + ']'.red;
+ grunt.log.error().writeln(pos + ' ' + (e.message + ' (position: ' + e.pos + ')').yellow);
+ grunt.warn('UglifyJS found errors.', 10);
+ }
});
- grunt.registerHelper('uglifySplitLines', function(src, maxLineLength) {
+ /**
+ * Try to split long lines so they have at most `maxLineLength` chars. Wrapper for Uglify's internal methods. Note:
+ * This helper is also used to split URLMap, do not remove it.
+ */
+ grunt.registerHelper('uglifySplitLines', function (src, maxLineLength) {
var pro = uglifyjs.uglify;
return pro.split_lines(src, maxLineLength || 32 * 1024);
});
View
113 build/grunt-tasks/helpers-uselesscoderemoval.js
@@ -0,0 +1,113 @@
+/**
+ * Helpers for useless code removal, to be used in min task (operating on JavaScript AST). The code that undergoes this
+ * operation is basically some description nodes in bean definitions, which can be useful at development time, but can
+ * be removed for production builds.
+ */
+module.exports = function (grunt) {
+
+ /**
+ * Helper to be used by the 'min' task to perform useless code removal on the AST obtained from UglifyJS.
+ */
+ grunt.registerHelper('removeUnnecessaryCode', function (ast) {
+ var savedBytes = replaceInPlaceRecursive(getAllReplacementVisitors(), ast, null);
+ if (savedBytes) {
+ grunt.helper('uselessCodeSavedBytes', savedBytes);
+ grunt.verbose.writeln("[min] Useless code removal: saved " + savedBytes + " bytes.");
+ grunt.verbose.writeln();
+ }
+ return ast;
+ });
+
+ /**
+ * Save in a global config the fact that we saved 'savedBytes' bytes (to be later read and displayed at the end of
+ * the build).
+ */
+ grunt.registerHelper('uselessCodeSavedBytes', function (savedBytes) {
+ var current = grunt.config.get('stats.uselessCodeSavedBytes') || 0;
+ grunt.config.set('stats.uselessCodeSavedBytes', current + savedBytes);
+ });
+
+ /**
+ * @param {Array<Function>} visitors list of functions to be executed on each array element of AST. Each of them is
+ * a <code>function (input, parents) : returns Number</code>.
+ * @param {Array|String} input element of the AST to be analyzed.
+ * @param {Array} parents parent arrays of the 'input' - used to backtrack in order to analyze the context (e.g.
+ * only to reset 'description' node if it is a child of '$events' in the JSON structure).
+ * @param {Number} depth used mainly for debugging
+ * @return {Number} number of bytes of code saved due to the operation
+ */
+ function replaceInPlaceRecursive (visitors, input, parents/*, depth*/) {
+ var savedBytes = 0;
+ // depth = depth || 0;
+ parents = parents || [];
+ var isArray = Array.isArray(input);
+ if (isArray) {
+ // console.log(depth + "/" + parents.length); // depth just for controlling
+ visitors.forEach(function (v) {
+ savedBytes += v(input, parents);
+ });
+
+ // create a new parents array by appending 'input' as the first element
+ var newParents = parents.slice(0); // copy
+ newParents.unshift(input); // unshift changes in-place (returns length, not the array -- can't chain!)
+
+ input.forEach(function (elem) {
+ savedBytes += replaceInPlaceRecursive(visitors, elem, newParents/*, depth+1*/);
+ });
+ }
+ return savedBytes;
+ };
+
+ // ============================== CONCRETE VISITORS ==========================
+ /**
+ * This replacer can be used to clear (set to an empty string) a string property of an object, given certain
+ * replacement condition. (if we have a string property in JSON, then input[1][0]=="string", input[1][1] == contents
+ * of the string, and input[0] == name of the JSON property in which that string is stored).
+ * @param {Function} fReplacementCondition function(input, parents): returns Boolean (true to perform the
+ * replacement)
+ * @param {String} comment comment to log if a replacement takes place.
+ * @param {Array} input a fragment of UglifyJS AST (current node)
+ * @param {Array<Array>} parents an array of fragments of UgliftJS AST's (parents of the current node).
+ * @return {Number} number of bytes saved due to string removal
+ */
+ var simpleStringCleaner = function (fReplacementCondition, comment, input, parents) {
+ var savedBytes = 0;
+ if (fReplacementCondition(input, parents) && input[1] && input[1][0] === "string") {
+ savedBytes = input[1][1].length;
+ if (savedBytes) { // do nothing if it was already empty
+ grunt.verbose.writeln(comment + input[1][1]);
+ input[1][1] = "";
+ }
+ }
+ return savedBytes;
+ };
+
+ /**
+ * A visitor which cleans all strings that are stored in a property named '$description'.
+ */
+ var removeDollarDescriptionInBean = function (input, parents) {
+ var condition = function (input, parents) {
+ return (input[0] === "$description");
+ };
+ return simpleStringCleaner(condition, '[min] Removed the $description: ', input, parents);
+ };
+
+ /**
+ * A visitor which cleans all strings that are stored in a property named 'description' which is a child of
+ * '$events' JSON object.
+ */
+ var removeDescriptionInEvent = function (input, parents) {
+ var condition = function (input, parents) {
+ return (input[0] === "description" && parents[5] && parents[5][0] === "$events");
+ };
+ return simpleStringCleaner(condition, '[min] Removed the description: ', input, parents);
+ };
+
+ function getAllReplacementVisitors () {
+ var replacementVisitors = [];
+ replacementVisitors.push(removeDollarDescriptionInBean);
+ replacementVisitors.push(removeDescriptionInEvent);
+ return replacementVisitors;
+ };
+
+};
View
4 build/grunt-tasks/task-hooks.js
@@ -1,11 +1,11 @@
module.exports = function(grunt) {
grunt.registerTask('gruntTimeHookStart', 'Hook to be executed before the first task', function() {
- grunt.config.set('gruntStartTimestamp', Date.now());
+ grunt.config.set('stats.gruntStartTimestamp', Date.now());
});
grunt.registerTask('gruntTimeHookEnd', 'Hook to be executed after the last task', function() {
- var started = grunt.config.get('gruntStartTimestamp');
+ var started = grunt.config.get('stats.gruntStartTimestamp');
var finished = Date.now();
var elapsed = ((finished - started) / 1000).toFixed(2);
View
13 build/grunt-tasks/task-removedcodestats.js
@@ -0,0 +1,13 @@
+/**
+ * Display how many bytes were saved in the unnecessary code removal process.
+ */
+module.exports = function (grunt) {
+
+ grunt.registerTask('removedCodeStats', 'Outputs the stats of unnecessary code removal done in min task.', function () {
+ var savedBytes = grunt.config.get('stats.uselessCodeSavedBytes');
+ var savedKb = (savedBytes / 1024).toFixed(1);
+
+ grunt.log.write('Useless code removal: saved total '.cyan + (savedKb + ' kB').yellow
+ + ' in non-gzipped code. '.cyan).ok();
+ });
+};
Something went wrong with that request. Please try again.