weightChecker.js 8.2 KB

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