Do not read file on the main process when using SHA-1 + experimental caches

Reviewed By: jeanlauliac

Differential Revision: D7443476

fbshipit-source-id: 4363b35ee5cbf988758b249c9a9c3d5ca606f317
This commit is contained in:
Miguel Jimenez Esun 2018-04-10 18:52:18 -07:00 committed by Facebook Github Bot
parent 8073bdaa08
commit e49c7a350c
7 changed files with 129 additions and 23 deletions

View File

@ -30,6 +30,7 @@ var fs = require('fs');
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const mkdirp = require('mkdirp'); const mkdirp = require('mkdirp');
const Module = require('../../node-haste/Module');
var commonOptions = { var commonOptions = {
allowBundleUpdates: false, allowBundleUpdates: false,
@ -117,4 +118,59 @@ describe('Bundler', function() {
expect(result.code).toEqual(minifiedCode); expect(result.code).toEqual(minifiedCode);
expect(result.map).toEqual([]); expect(result.map).toEqual([]);
}); });
it('uses new cache layers when transforming if requested to do so', async () => {
const get = jest.fn();
const set = jest.fn();
const bundlerInstance = new Bundler({
...commonOptions,
cacheStores: [{get, set}],
projectRoots,
});
const depGraph = {
getSha1: jest.fn(() => '0123456789012345678901234567890123456789'),
};
jest.spyOn(bundlerInstance, 'getDependencyGraph').mockImplementation(() => {
return new Promise(resolve => {
resolve(depGraph);
});
});
const module = new Module({
file: '/root/foo.js',
localPath: 'foo.js',
experimentalCaches: true,
});
require('../../JSTransformer').prototype.transform.mockReturnValue({
sha1: 'abcdefabcdefabcdefabcdefabcdefabcdefabcd',
result: {},
});
await bundlerInstance._cachedTransformCode(module, null, {});
// We got the SHA-1 of the file from the dependency graph.
expect(depGraph.getSha1).toBeCalledWith('/root/foo.js');
// Only one get, with the original SHA-1.
expect(get).toHaveBeenCalledTimes(1);
expect(get.mock.calls[0][0].toString('hex')).toMatch(
'0123456789012345678901234567890123456789',
);
// Only one set, with the *modified* SHA-1. This happens when the file gets
// modified between querying the caches and saving.
expect(set).toHaveBeenCalledTimes(1);
expect(set.mock.calls[0][0].toString('hex')).toMatch(
'abcdefabcdefabcdefabcdefabcdefabcdefabcd',
);
// But, the common part of the key remains the same.
expect(get.mock.calls[0][0].toString('hex').substr(0, 32)).toBe(
set.mock.calls[0][0].toString('hex').substr(0, 32),
);
});
}); });

View File

@ -257,12 +257,14 @@ class Bundler {
async _cachedTransformCode( async _cachedTransformCode(
module: Module, module: Module,
code: string, code: ?string,
transformCodeOptions: WorkerOptions, transformCodeOptions: WorkerOptions,
): Promise<TransformedCode> { ): Promise<TransformedCode> {
const cache = this._cache; const cache = this._cache;
let result; let data = null;
let key; let partialKey;
let fullKey;
let sha1;
// First, try getting the result from the cache if enabled. // First, try getting the result from the cache if enabled.
if (cache) { if (cache) {
@ -287,13 +289,12 @@ class Bundler {
} }
} }
key = stableHash([ partialKey = stableHash([
// This is the hash related to the global Bundler config. // This is the hash related to the global Bundler config.
this._baseHash, this._baseHash,
// Path and code hash. // Path and code hash.
module.localPath, module.localPath,
(await this.getDependencyGraph()).getSha1(module.path),
// We cannot include "transformCodeOptions" because of "projectRoot". // We cannot include "transformCodeOptions" because of "projectRoot".
assetDataPlugins, assetDataPlugins,
@ -306,12 +307,25 @@ class Bundler {
platform, platform,
]); ]);
result = await cache.get(key); sha1 = (await this.getDependencyGraph()).getSha1(module.path);
fullKey = Buffer.concat([partialKey, Buffer.from(sha1, 'hex')]);
const result = await cache.get(fullKey);
if (result) {
data = {result, sha1};
}
}
if (!cache && code == null) {
throw new Error(
'When not using experimental caches, code should always be provided',
);
} }
// Second, if there was no result, compute it ourselves. // Second, if there was no result, compute it ourselves.
if (!result) { if (!data) {
result = await this._transformer.transform( data = await this._transformer.transform(
module.path, module.path,
module.localPath, module.localPath,
code, code,
@ -323,11 +337,17 @@ class Bundler {
} }
// Third, propagate the result to all cache layers. // Third, propagate the result to all cache layers.
if (key && cache) { if (fullKey && partialKey && sha1 && cache) {
cache.set(key, result); // It could be that the SHA-1 we had back when we sent to the transformer
// was outdated; if so, recompute it.
if (sha1 !== data.sha1) {
fullKey = Buffer.concat([partialKey, Buffer.from(data.sha1, 'hex')]);
} }
return result; cache.set(fullKey, data.result);
}
return data.result;
} }
} }

View File

@ -55,6 +55,7 @@ describe('Transformer', function() {
api.transform.mockImplementation(() => { api.transform.mockImplementation(() => {
return { return {
result: 'transformed(code)', result: 'transformed(code)',
sha1: '4ea962697c876e2674d107f0fec6798414f5bf45',
transformFileStartLogEntry: {}, transformFileStartLogEntry: {},
transformFileEndLogEntry: {}, transformFileEndLogEntry: {},
}; };

View File

@ -35,6 +35,11 @@ type Reporters = {
+stderrChunk: (chunk: string) => mixed, +stderrChunk: (chunk: string) => mixed,
}; };
type TransformerResult = {
result: TransformedCode,
sha1: string,
};
module.exports = class Transformer { module.exports = class Transformer {
_worker: WorkerInterface; _worker: WorkerInterface;
_transformModulePath: string; _transformModulePath: string;
@ -101,12 +106,12 @@ module.exports = class Transformer {
async transform( async transform(
filename: string, filename: string,
localPath: LocalPath, localPath: LocalPath,
code: string, code: ?string,
isScript: boolean, isScript: boolean,
options: Options, options: Options,
assetExts: $ReadOnlyArray<string>, assetExts: $ReadOnlyArray<string>,
assetRegistryPath: string, assetRegistryPath: string,
): Promise<TransformedCode> { ): Promise<TransformerResult> {
try { try {
debug('Started transforming file', filename); debug('Started transforming file', filename);
@ -128,7 +133,10 @@ module.exports = class Transformer {
Logger.log(data.transformFileStartLogEntry); Logger.log(data.transformFileStartLogEntry);
Logger.log(data.transformFileEndLogEntry); Logger.log(data.transformFileEndLogEntry);
return data.result; return {
result: data.result,
sha1: Buffer.from(data.sha1, 'hex'),
};
} catch (err) { } catch (err) {
debug('Failed transform file', filename); debug('Failed transform file', filename);

View File

@ -15,6 +15,8 @@ const JsFileWrapping = require('../../ModuleGraph/worker/JsFileWrapping');
const assetTransformer = require('../../assetTransformer'); const assetTransformer = require('../../assetTransformer');
const collectDependencies = require('../../ModuleGraph/worker/collectDependencies'); const collectDependencies = require('../../ModuleGraph/worker/collectDependencies');
const constantFoldingPlugin = require('./constant-folding-plugin'); const constantFoldingPlugin = require('./constant-folding-plugin');
const crypto = require('crypto');
const fs = require('fs');
const getMinifier = require('../../lib/getMinifier'); const getMinifier = require('../../lib/getMinifier');
const inlinePlugin = require('./inline-plugin'); const inlinePlugin = require('./inline-plugin');
const optimizeDependencies = require('../../ModuleGraph/worker/optimizeDependencies'); const optimizeDependencies = require('../../ModuleGraph/worker/optimizeDependencies');
@ -90,6 +92,7 @@ export type Options = TransformOptionsStrict;
export type Data = { export type Data = {
result: TransformedCode, result: TransformedCode,
sha1: string,
transformFileStartLogEntry: LogEntry, transformFileStartLogEntry: LogEntry,
transformFileEndLogEntry: LogEntry, transformFileEndLogEntry: LogEntry,
}; };
@ -115,7 +118,7 @@ function getDynamicDepsBehavior(
async function transformCode( async function transformCode(
filename: string, filename: string,
localPath: LocalPath, localPath: LocalPath,
sourceCode: string, sourceCode: ?string,
transformerPath: string, transformerPath: string,
isScript: boolean, isScript: boolean,
options: Options, options: Options,
@ -124,6 +127,10 @@ async function transformCode(
asyncRequireModulePath: string, asyncRequireModulePath: string,
dynamicDepsInPackages: DynamicRequiresBehavior, dynamicDepsInPackages: DynamicRequiresBehavior,
): Promise<Data> { ): Promise<Data> {
if (sourceCode == null) {
sourceCode = fs.readFileSync(filename, 'utf8');
}
const transformFileStartLogEntry = { const transformFileStartLogEntry = {
action_name: 'Transforming file', action_name: 'Transforming file',
action_phase: 'start', action_phase: 'start',
@ -132,6 +139,11 @@ async function transformCode(
start_timestamp: process.hrtime(), start_timestamp: process.hrtime(),
}; };
const sha1 = crypto
.createHash('sha1')
.update(sourceCode)
.digest('hex');
if (filename.endsWith('.json')) { if (filename.endsWith('.json')) {
const code = JsFileWrapping.wrapJson(sourceCode); const code = JsFileWrapping.wrapJson(sourceCode);
@ -142,6 +154,7 @@ async function transformCode(
return { return {
result: {dependencies: [], code, map: []}, result: {dependencies: [], code, map: []},
sha1,
transformFileStartLogEntry, transformFileStartLogEntry,
transformFileEndLogEntry, transformFileEndLogEntry,
}; };
@ -241,6 +254,7 @@ async function transformCode(
return { return {
result: {dependencies, code: result.code, map}, result: {dependencies, code: result.code, map},
sha1,
transformFileStartLogEntry, transformFileStartLogEntry,
transformFileEndLogEntry, transformFileEndLogEntry,
}; };

View File

@ -45,7 +45,7 @@ export type CachedReadResult = ?ReadResult;
export type TransformCode = ( export type TransformCode = (
module: Module, module: Module,
sourceCode: string, sourceCode: ?string,
transformOptions: WorkerOptions, transformOptions: WorkerOptions,
) => Promise<TransformedCode>; ) => Promise<TransformedCode>;
@ -304,12 +304,18 @@ class Module {
// TODO: T26134860 Cache layer lives inside the transformer now; just call // TODO: T26134860 Cache layer lives inside the transformer now; just call
// the transform method. // the transform method.
if (this._experimentalCaches) { if (this._experimentalCaches) {
const sourceCode = this._readSourceCode(); // Source code is read on the worker.
const data = {
return { ...(await this._transformCode(this, null, transformOptions)),
...(await this._transformCode(this, sourceCode, transformOptions)),
sourceCode,
}; };
// eslint-disable-next-line lint/flow-no-fixme
// $FlowFixMe: Flow wants "value" here, where the get is for AVOIDING it.
Object.defineProperty(data, 'sourceCode', {
get: () => this._readSourceCode.bind(this),
});
return data;
} }
const cached = this.readCached(transformOptions); const cached = this.readCached(transformOptions);

View File

@ -120,8 +120,9 @@ describe('Module', () => {
expect(res2.dependencies).toEqual(['dep1', 'dep2']); expect(res2.dependencies).toEqual(['dep1', 'dep2']);
expect(transformCode).toHaveBeenCalledTimes(2); expect(transformCode).toHaveBeenCalledTimes(2);
// Code was only read once, though. // Code was never read, though, because experimental caches read on the
expect(fs.readFileSync).toHaveBeenCalledTimes(1); // worker, to speed up local cache!
expect(fs.readFileSync).not.toHaveBeenCalled();
}); });
}); });