weightChecker.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. /*
  2. * Redownloading every files after Phantomas has finished
  3. * Checks weight and every kind of compression
  4. *
  5. */
  6. var debug = require('debug')('ylt:weightChecker');
  7. var Q = require('q');
  8. var http = require('http');
  9. var zlib = require('zlib');
  10. var async = require('async');
  11. var request = require('request');
  12. var WeightChecker = function() {
  13. var MAX_PARALLEL_DOWNLOADS = 10;
  14. var REQUEST_TIMEOUT = 10000; // 10 seconds
  15. function recheckAllFiles(data) {
  16. var deferred = Q.defer();
  17. var requestsList = JSON.parse(data.toolsResults.phantomas.offenders.requestsList);
  18. delete data.toolsResults.phantomas.metrics.requestsList;
  19. delete data.toolsResults.phantomas.offenders.requestsList;
  20. // Transform every request into a download function with a callback when done
  21. var redownloadList = requestsList.map(function(entry) {
  22. return function(callback) {
  23. redownloadEntry(entry)
  24. .then(recompressIfImage)
  25. .then(function(entry) {
  26. callback(null, entry);
  27. })
  28. .fail(function(err) {
  29. callback(err);
  30. });
  31. };
  32. });
  33. // Lanch all redownload functions and wait for completion
  34. async.parallelLimit(redownloadList, MAX_PARALLEL_DOWNLOADS, function(err, results) {
  35. if (err) {
  36. debug(err);
  37. deferred.reject(err);
  38. } else {
  39. debug('All files checked');
  40. var metrics = {};
  41. var offenders = {};
  42. // Total weight
  43. offenders.totalWeight = listRequestWeight(results);
  44. metrics.totalWeight = offenders.totalWeight.totalWeight;
  45. data.toolsResults.weightChecker = {
  46. metrics: metrics,
  47. offenders: offenders
  48. };
  49. deferred.resolve(data);
  50. }
  51. });
  52. return deferred.promise;
  53. }
  54. function listRequestWeight(requests) {
  55. var results = {
  56. totalWeight: 0,
  57. byType: {
  58. html: {
  59. totalWeight: 0,
  60. requests: []
  61. },
  62. css: {
  63. totalWeight: 0,
  64. requests: []
  65. },
  66. js: {
  67. totalWeight: 0,
  68. requests: []
  69. },
  70. json: {
  71. totalWeight: 0,
  72. requests: []
  73. },
  74. image: {
  75. totalWeight: 0,
  76. requests: []
  77. },
  78. video: {
  79. totalWeight: 0,
  80. requests: []
  81. },
  82. webfont: {
  83. totalWeight: 0,
  84. requests: []
  85. },
  86. other: {
  87. totalWeight: 0,
  88. requests: []
  89. }
  90. }
  91. };
  92. requests.forEach(function(req) {
  93. var weight = ((typeof req.weightCheck.bodySize === 'number') ? req.weightCheck.bodySize + req.weightCheck.headersSize : req.contentLength) || 0;
  94. var type = req.type || 'other';
  95. results.totalWeight += weight;
  96. results.byType[type].totalWeight += weight;
  97. results.byType[type].requests.push({
  98. url: req.url,
  99. weight: weight
  100. });
  101. });
  102. return results;
  103. }
  104. function redownloadEntry(entry) {
  105. var deferred = Q.defer();
  106. function onError(message) {
  107. debug('Could not download %s Error: %s', entry.url, message);
  108. entry.weightCheck = {
  109. message: message
  110. };
  111. deferred.reject();
  112. }
  113. if (entry.method !== 'GET') {
  114. onError('only downloading GET');
  115. return deferred.promise;
  116. }
  117. if (entry.status !== 200) {
  118. onError('only downloading requests with status code 200');
  119. return deferred.promise;
  120. }
  121. debug('Downloading %s', entry.url);
  122. // Always add a gzip header before sending, in case the server listens to it
  123. var reqHeaders = entry.requestHeaders;
  124. reqHeaders['Accept-Encoding'] = 'gzip, deflate';
  125. var requestOptions = {
  126. method: entry.method,
  127. url: entry.url,
  128. headers: reqHeaders,
  129. timeout: REQUEST_TIMEOUT
  130. };
  131. download(requestOptions, function(error, result) {
  132. if (error) {
  133. if (error.code === 'ETIMEDOUT') {
  134. onError('timeout after ' + REQUEST_TIMEOUT + 'ms');
  135. } else {
  136. onError('error while downloading: ' + error.code);
  137. }
  138. return;
  139. }
  140. debug('%s downloaded correctly', entry.url);
  141. entry.weightCheck = result;
  142. deferred.resolve(entry);
  143. });
  144. return deferred.promise;
  145. }
  146. // Inspired by https://github.com/cvan/fastHAR-api/blob/10cec585/app.js
  147. function download(requestOptions, callback) {
  148. var statusCode;
  149. request(requestOptions)
  150. .on('response', function(res) {
  151. // Raw headers were added in NodeJS v0.12
  152. // (https://github.com/joyent/node/issues/4844), but let's
  153. // reconstruct them for backwards compatibility.
  154. var rawHeaders = ('HTTP/' + res.httpVersion + ' ' + res.statusCode +
  155. ' ' + http.STATUS_CODES[res.statusCode] + '\r\n');
  156. Object.keys(res.headers).forEach(function(headerKey) {
  157. rawHeaders += headerKey + ': ' + res.headers[headerKey] + '\r\n';
  158. });
  159. rawHeaders += '\r\n';
  160. var uncompressedSize = 0; // size after uncompression
  161. var bodySize = 0; // bytes size over the wire
  162. var body = ''; // plain text body (after uncompressing gzip/deflate)
  163. var isCompressed = false;
  164. function tally() {
  165. if (statusCode !== 200) {
  166. callback({code: statusCode});
  167. return;
  168. }
  169. var result = {
  170. body: body,
  171. headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
  172. bodySize: bodySize,
  173. isCompressed: isCompressed,
  174. uncompressedSize: uncompressedSize
  175. };
  176. callback(null, result);
  177. }
  178. switch (res.headers['content-encoding']) {
  179. case 'gzip':
  180. var gzip = zlib.createGunzip();
  181. gzip.on('data', function (data) {
  182. body += data;
  183. uncompressedSize += data.length;
  184. }).on('end', function () {
  185. isCompressed = true;
  186. tally();
  187. });
  188. res.on('data', function (data) {
  189. bodySize += data.length;
  190. }).pipe(gzip);
  191. break;
  192. case 'deflate':
  193. var deflate = zlib.createInflate();
  194. deflate.on('data', function (data) {
  195. body += data;
  196. uncompressedSize += data.length;
  197. }).on('end', function () {
  198. isCompressed = true;
  199. tally();
  200. });
  201. res.on('data', function (data) {
  202. bodySize += data.length;
  203. }).pipe(deflate);
  204. break;
  205. default:
  206. res.on('data', function (data) {
  207. body += data;
  208. uncompressedSize += data.length;
  209. bodySize += data.length;
  210. }).on('end', function () {
  211. tally();
  212. });
  213. break;
  214. }
  215. })
  216. .on('response', function(response) {
  217. statusCode = response.statusCode;
  218. })
  219. .on('error', function(err) {
  220. callback(err);
  221. });
  222. }
  223. function recompressIfImage(entry) {
  224. var deferred = Q.defer();
  225. deferred.resolve(entry);
  226. return deferred.promise;
  227. }
  228. return {
  229. recheckAllFiles: recheckAllFiles,
  230. listRequestWeight: listRequestWeight,
  231. redownloadEntry: redownloadEntry,
  232. download: download,
  233. recompressIfImage: recompressIfImage
  234. };
  235. };
  236. module.exports = new WeightChecker();