v8-compile-cache.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. 'use strict';
  2. const Module = require('module');
  3. const crypto = require('crypto');
  4. const fs = require('fs');
  5. const path = require('path');
  6. const vm = require('vm');
  7. const os = require('os');
  8. const hasOwnProperty = Object.prototype.hasOwnProperty;
  9. //------------------------------------------------------------------------------
  10. // FileSystemBlobStore
  11. //------------------------------------------------------------------------------
  12. class FileSystemBlobStore {
  13. constructor(directory, prefix) {
  14. const name = prefix ? slashEscape(prefix + '.') : '';
  15. this._blobFilename = path.join(directory, name + 'BLOB');
  16. this._mapFilename = path.join(directory, name + 'MAP');
  17. this._lockFilename = path.join(directory, name + 'LOCK');
  18. this._directory = directory;
  19. this._load();
  20. }
  21. has(key, invalidationKey) {
  22. if (hasOwnProperty.call(this._memoryBlobs, key)) {
  23. return this._invalidationKeys[key] === invalidationKey;
  24. } else if (hasOwnProperty.call(this._storedMap, key)) {
  25. return this._storedMap[key][0] === invalidationKey;
  26. }
  27. return false;
  28. }
  29. get(key, invalidationKey) {
  30. if (hasOwnProperty.call(this._memoryBlobs, key)) {
  31. if (this._invalidationKeys[key] === invalidationKey) {
  32. return this._memoryBlobs[key];
  33. }
  34. } else if (hasOwnProperty.call(this._storedMap, key)) {
  35. const mapping = this._storedMap[key];
  36. if (mapping[0] === invalidationKey) {
  37. return this._storedBlob.slice(mapping[1], mapping[2]);
  38. }
  39. }
  40. }
  41. set(key, invalidationKey, buffer) {
  42. this._invalidationKeys[key] = invalidationKey;
  43. this._memoryBlobs[key] = buffer;
  44. this._dirty = true;
  45. }
  46. delete(key) {
  47. if (hasOwnProperty.call(this._memoryBlobs, key)) {
  48. this._dirty = true;
  49. delete this._memoryBlobs[key];
  50. }
  51. if (hasOwnProperty.call(this._invalidationKeys, key)) {
  52. this._dirty = true;
  53. delete this._invalidationKeys[key];
  54. }
  55. if (hasOwnProperty.call(this._storedMap, key)) {
  56. this._dirty = true;
  57. delete this._storedMap[key];
  58. }
  59. }
  60. isDirty() {
  61. return this._dirty;
  62. }
  63. save() {
  64. const dump = this._getDump();
  65. const blobToStore = Buffer.concat(dump[0]);
  66. const mapToStore = JSON.stringify(dump[1]);
  67. try {
  68. mkdirpSync(this._directory);
  69. fs.writeFileSync(this._lockFilename, 'LOCK', {flag: 'wx'});
  70. } catch (error) {
  71. // Swallow the exception if we fail to acquire the lock.
  72. return false;
  73. }
  74. try {
  75. fs.writeFileSync(this._blobFilename, blobToStore);
  76. fs.writeFileSync(this._mapFilename, mapToStore);
  77. } finally {
  78. fs.unlinkSync(this._lockFilename);
  79. }
  80. return true;
  81. }
  82. _load() {
  83. try {
  84. this._storedBlob = fs.readFileSync(this._blobFilename);
  85. this._storedMap = JSON.parse(fs.readFileSync(this._mapFilename));
  86. } catch (e) {
  87. this._storedBlob = Buffer.alloc(0);
  88. this._storedMap = {};
  89. }
  90. this._dirty = false;
  91. this._memoryBlobs = {};
  92. this._invalidationKeys = {};
  93. }
  94. _getDump() {
  95. const buffers = [];
  96. const newMap = {};
  97. let offset = 0;
  98. function push(key, invalidationKey, buffer) {
  99. buffers.push(buffer);
  100. newMap[key] = [invalidationKey, offset, offset + buffer.length];
  101. offset += buffer.length;
  102. }
  103. for (const key of Object.keys(this._memoryBlobs)) {
  104. const buffer = this._memoryBlobs[key];
  105. const invalidationKey = this._invalidationKeys[key];
  106. push(key, invalidationKey, buffer);
  107. }
  108. for (const key of Object.keys(this._storedMap)) {
  109. if (hasOwnProperty.call(newMap, key)) continue;
  110. const mapping = this._storedMap[key];
  111. const buffer = this._storedBlob.slice(mapping[1], mapping[2]);
  112. push(key, mapping[0], buffer);
  113. }
  114. return [buffers, newMap];
  115. }
  116. }
  117. //------------------------------------------------------------------------------
  118. // NativeCompileCache
  119. //------------------------------------------------------------------------------
  120. class NativeCompileCache {
  121. constructor() {
  122. this._cacheStore = null;
  123. this._previousModuleCompile = null;
  124. }
  125. setCacheStore(cacheStore) {
  126. this._cacheStore = cacheStore;
  127. }
  128. install() {
  129. const self = this;
  130. const hasRequireResolvePaths = typeof require.resolve.paths === 'function';
  131. this._previousModuleCompile = Module.prototype._compile;
  132. Module.prototype._compile = function(content, filename) {
  133. const mod = this;
  134. function require(id) {
  135. return mod.require(id);
  136. }
  137. // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L28
  138. function resolve(request, options) {
  139. return Module._resolveFilename(request, mod, false, options);
  140. }
  141. require.resolve = resolve;
  142. // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L37
  143. // resolve.resolve.paths was added in v8.9.0
  144. if (hasRequireResolvePaths) {
  145. resolve.paths = function paths(request) {
  146. return Module._resolveLookupPaths(request, mod, true);
  147. };
  148. }
  149. require.main = process.mainModule;
  150. // Enable support to add extra extension types
  151. require.extensions = Module._extensions;
  152. require.cache = Module._cache;
  153. const dirname = path.dirname(filename);
  154. const compiledWrapper = self._moduleCompile(filename, content);
  155. // We skip the debugger setup because by the time we run, node has already
  156. // done that itself.
  157. // `Buffer` is included for Electron.
  158. // See https://github.com/zertosh/v8-compile-cache/pull/10#issuecomment-518042543
  159. const args = [mod.exports, require, mod, filename, dirname, process, global, Buffer];
  160. return compiledWrapper.apply(mod.exports, args);
  161. };
  162. }
  163. uninstall() {
  164. Module.prototype._compile = this._previousModuleCompile;
  165. }
  166. _moduleCompile(filename, content) {
  167. // https://github.com/nodejs/node/blob/v7.5.0/lib/module.js#L511
  168. // Remove shebang
  169. var contLen = content.length;
  170. if (contLen >= 2) {
  171. if (content.charCodeAt(0) === 35/*#*/ &&
  172. content.charCodeAt(1) === 33/*!*/) {
  173. if (contLen === 2) {
  174. // Exact match
  175. content = '';
  176. } else {
  177. // Find end of shebang line and slice it off
  178. var i = 2;
  179. for (; i < contLen; ++i) {
  180. var code = content.charCodeAt(i);
  181. if (code === 10/*\n*/ || code === 13/*\r*/) break;
  182. }
  183. if (i === contLen) {
  184. content = '';
  185. } else {
  186. // Note that this actually includes the newline character(s) in the
  187. // new output. This duplicates the behavior of the regular
  188. // expression that was previously used to replace the shebang line
  189. content = content.slice(i);
  190. }
  191. }
  192. }
  193. }
  194. // create wrapper function
  195. var wrapper = Module.wrap(content);
  196. var invalidationKey = crypto
  197. .createHash('sha1')
  198. .update(content, 'utf8')
  199. .digest('hex');
  200. var buffer = this._cacheStore.get(filename, invalidationKey);
  201. var script = new vm.Script(wrapper, {
  202. filename: filename,
  203. lineOffset: 0,
  204. displayErrors: true,
  205. cachedData: buffer,
  206. produceCachedData: true,
  207. });
  208. if (script.cachedDataProduced) {
  209. this._cacheStore.set(filename, invalidationKey, script.cachedData);
  210. } else if (script.cachedDataRejected) {
  211. this._cacheStore.delete(filename);
  212. }
  213. var compiledWrapper = script.runInThisContext({
  214. filename: filename,
  215. lineOffset: 0,
  216. columnOffset: 0,
  217. displayErrors: true,
  218. });
  219. return compiledWrapper;
  220. }
  221. }
  222. //------------------------------------------------------------------------------
  223. // utilities
  224. //
  225. // https://github.com/substack/node-mkdirp/blob/f2003bb/index.js#L55-L98
  226. // https://github.com/zertosh/slash-escape/blob/e7ebb99/slash-escape.js
  227. //------------------------------------------------------------------------------
  228. function mkdirpSync(p_) {
  229. _mkdirpSync(path.resolve(p_), 0o777);
  230. }
  231. function _mkdirpSync(p, mode) {
  232. try {
  233. fs.mkdirSync(p, mode);
  234. } catch (err0) {
  235. if (err0.code === 'ENOENT') {
  236. _mkdirpSync(path.dirname(p));
  237. _mkdirpSync(p);
  238. } else {
  239. try {
  240. const stat = fs.statSync(p);
  241. if (!stat.isDirectory()) { throw err0; }
  242. } catch (err1) {
  243. throw err0;
  244. }
  245. }
  246. }
  247. }
  248. function slashEscape(str) {
  249. const ESCAPE_LOOKUP = {
  250. '\\': 'zB',
  251. ':': 'zC',
  252. '/': 'zS',
  253. '\x00': 'z0',
  254. 'z': 'zZ',
  255. };
  256. const ESCAPE_REGEX = /[\\:/\x00z]/g; // eslint-disable-line no-control-regex
  257. return str.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]);
  258. }
  259. function supportsCachedData() {
  260. const script = new vm.Script('""', {produceCachedData: true});
  261. // chakracore, as of v1.7.1.0, returns `false`.
  262. return script.cachedDataProduced === true;
  263. }
  264. function getCacheDir() {
  265. const v8_compile_cache_cache_dir = process.env.V8_COMPILE_CACHE_CACHE_DIR;
  266. if (v8_compile_cache_cache_dir) {
  267. return v8_compile_cache_cache_dir;
  268. }
  269. // Avoid cache ownership issues on POSIX systems.
  270. const dirname = typeof process.getuid === 'function'
  271. ? 'v8-compile-cache-' + process.getuid()
  272. : 'v8-compile-cache';
  273. // Avoid cache incompatibility issues with Rosetta on Apple Silicon.
  274. const arch = process.arch;
  275. const version = typeof process.versions.v8 === 'string'
  276. ? process.versions.v8
  277. : typeof process.versions.chakracore === 'string'
  278. ? 'chakracore-' + process.versions.chakracore
  279. : 'node-' + process.version;
  280. const cacheDir = path.join(os.tmpdir(), dirname, arch, version);
  281. return cacheDir;
  282. }
  283. function getMainName() {
  284. // `require.main.filename` is undefined or null when:
  285. // * node -e 'require("v8-compile-cache")'
  286. // * node -r 'v8-compile-cache'
  287. // * Or, requiring from the REPL.
  288. const mainName = require.main && typeof require.main.filename === 'string'
  289. ? require.main.filename
  290. : process.cwd();
  291. return mainName;
  292. }
  293. //------------------------------------------------------------------------------
  294. // main
  295. //------------------------------------------------------------------------------
  296. if (!process.env.DISABLE_V8_COMPILE_CACHE && supportsCachedData()) {
  297. const cacheDir = getCacheDir();
  298. const prefix = getMainName();
  299. const blobStore = new FileSystemBlobStore(cacheDir, prefix);
  300. const nativeCompileCache = new NativeCompileCache();
  301. nativeCompileCache.setCacheStore(blobStore);
  302. nativeCompileCache.install();
  303. process.once('exit', () => {
  304. if (blobStore.isDirty()) {
  305. blobStore.save();
  306. }
  307. nativeCompileCache.uninstall();
  308. });
  309. }
  310. module.exports.__TEST__ = {
  311. FileSystemBlobStore,
  312. NativeCompileCache,
  313. mkdirpSync,
  314. slashEscape,
  315. supportsCachedData,
  316. getCacheDir,
  317. getMainName,
  318. };