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 path = require('path');
const mkdirp = require('mkdirp');
const Module = require('../../node-haste/Module');
var commonOptions = {
allowBundleUpdates: false,
@ -117,4 +118,59 @@ describe('Bundler', function() {
expect(result.code).toEqual(minifiedCode);
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(
module: Module,
code: string,
code: ?string,
transformCodeOptions: WorkerOptions,
): Promise<TransformedCode> {
const cache = this._cache;
let result;
let key;
let data = null;
let partialKey;
let fullKey;
let sha1;
// First, try getting the result from the cache if enabled.
if (cache) {
@ -287,13 +289,12 @@ class Bundler {
}
}
key = stableHash([
partialKey = stableHash([
// This is the hash related to the global Bundler config.
this._baseHash,
// Path and code hash.
module.localPath,
(await this.getDependencyGraph()).getSha1(module.path),
// We cannot include "transformCodeOptions" because of "projectRoot".
assetDataPlugins,
@ -306,12 +307,25 @@ class Bundler {
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.
if (!result) {
result = await this._transformer.transform(
if (!data) {
data = await this._transformer.transform(
module.path,
module.localPath,
code,
@ -323,11 +337,17 @@ class Bundler {
}
// Third, propagate the result to all cache layers.
if (key && cache) {
cache.set(key, result);
if (fullKey && partialKey && sha1 && cache) {
// 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')]);
}
cache.set(fullKey, data.result);
}
return result;
return data.result;
}
}

View File

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

View File

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

View File

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

View File

@ -45,7 +45,7 @@ export type CachedReadResult = ?ReadResult;
export type TransformCode = (
module: Module,
sourceCode: string,
sourceCode: ?string,
transformOptions: WorkerOptions,
) => Promise<TransformedCode>;
@ -304,12 +304,18 @@ class Module {
// TODO: T26134860 Cache layer lives inside the transformer now; just call
// the transform method.
if (this._experimentalCaches) {
const sourceCode = this._readSourceCode();
return {
...(await this._transformCode(this, sourceCode, transformOptions)),
sourceCode,
// Source code is read on the worker.
const data = {
...(await this._transformCode(this, null, transformOptions)),
};
// 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);

View File

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