packager: index files by dir for fast matching

Summary: This removes the call to `HasteFS#matchFiles()`, that has linear complexity. Instead, we index all the files by directory from `HasteFS` once loaded, a linear operation. Then, we can filter files from a particular directory much quicker.

Reviewed By: davidaurelio

Differential Revision: D4826721

fbshipit-source-id: c31a0ed9a354dbc7f2dcd56179b859e491faa16c
This commit is contained in:
Jean Lauliac 2017-04-06 07:45:36 -07:00 committed by Facebook Github Bot
parent d1ea86b84c
commit 8d34f16a70
4 changed files with 96 additions and 13 deletions

View File

@ -22,6 +22,7 @@ import type {
} from '../types.flow'; } from '../types.flow';
const DependencyGraphHelpers = require('../../node-haste/DependencyGraph/DependencyGraphHelpers'); const DependencyGraphHelpers = require('../../node-haste/DependencyGraph/DependencyGraphHelpers');
const FilesByDirNameIndex = require('../../node-haste/FilesByDirNameIndex');
const HasteFS = require('./HasteFS'); const HasteFS = require('./HasteFS');
const HasteMap = require('../../node-haste/DependencyGraph/HasteMap'); const HasteMap = require('../../node-haste/DependencyGraph/HasteMap');
const Module = require('./Module'); const Module = require('./Module');
@ -92,6 +93,7 @@ exports.createResolveFn = function(options: ResolveOptions): ResolveFn {
const hasteMapBuilt = hasteMap.build(); const hasteMapBuilt = hasteMap.build();
const resolutionRequests = {}; const resolutionRequests = {};
const filesByDirNameIndex = new FilesByDirNameIndex(hasteMap.getAllFiles());
return (id, source, platform, _, callback) => { return (id, source, platform, _, callback) => {
let resolutionRequest = resolutionRequests[platform]; let resolutionRequest = resolutionRequests[platform];
if (!resolutionRequest) { if (!resolutionRequest) {
@ -102,6 +104,7 @@ exports.createResolveFn = function(options: ResolveOptions): ResolveFn {
hasteFS, hasteFS,
hasteMap, hasteMap,
helpers, helpers,
matchFiles: filesByDirNameIndex.match.bind(filesByDirNameIndex),
moduleCache, moduleCache,
moduleMap: getFakeModuleMap(hasteMap), moduleMap: getFakeModuleMap(hasteMap),
platform, platform,

View File

@ -53,12 +53,15 @@ type ModuleishCache<TModule, TPackage> = {
getAssetModule(path: string): TModule, getAssetModule(path: string): TModule,
}; };
type MatchFilesByDirAndPattern = (dirName: string, pattern: RegExp) => Array<string>;
type Options<TModule, TPackage> = { type Options<TModule, TPackage> = {
dirExists: DirExistsFn, dirExists: DirExistsFn,
entryPath: string, entryPath: string,
extraNodeModules: ?Object, extraNodeModules: ?Object,
hasteFS: HasteFS, hasteFS: HasteFS,
helpers: DependencyGraphHelpers, helpers: DependencyGraphHelpers,
matchFiles: MatchFilesByDirAndPattern,
moduleCache: ModuleishCache<TModule, TPackage>, moduleCache: ModuleishCache<TModule, TPackage>,
moduleMap: ModuleMap, moduleMap: ModuleMap,
platform: string, platform: string,
@ -89,6 +92,7 @@ class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
_hasteFS: HasteFS; _hasteFS: HasteFS;
_helpers: DependencyGraphHelpers; _helpers: DependencyGraphHelpers;
_immediateResolutionCache: {[key: string]: TModule}; _immediateResolutionCache: {[key: string]: TModule};
_matchFiles: MatchFilesByDirAndPattern;
_moduleCache: ModuleishCache<TModule, TPackage>; _moduleCache: ModuleishCache<TModule, TPackage>;
_moduleMap: ModuleMap; _moduleMap: ModuleMap;
_platform: string; _platform: string;
@ -102,6 +106,7 @@ class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
extraNodeModules, extraNodeModules,
hasteFS, hasteFS,
helpers, helpers,
matchFiles,
moduleCache, moduleCache,
moduleMap, moduleMap,
platform, platform,
@ -113,6 +118,7 @@ class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
this._extraNodeModules = extraNodeModules; this._extraNodeModules = extraNodeModules;
this._hasteFS = hasteFS; this._hasteFS = hasteFS;
this._helpers = helpers; this._helpers = helpers;
this._matchFiles = matchFiles;
this._moduleCache = moduleCache; this._moduleCache = moduleCache;
this._moduleMap = moduleMap; this._moduleMap = moduleMap;
this._platform = platform; this._platform = platform;
@ -442,7 +448,7 @@ class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
_loadAsFile(potentialModulePath: string, fromModule: TModule, toModule: string): TModule { _loadAsFile(potentialModulePath: string, fromModule: TModule, toModule: string): TModule {
if (this._helpers.isAssetFile(potentialModulePath)) { if (this._helpers.isAssetFile(potentialModulePath)) {
let dirname = path.dirname(potentialModulePath); const dirname = path.dirname(potentialModulePath);
if (!this._dirExists(dirname)) { if (!this._dirExists(dirname)) {
throw new UnableToResolveError( throw new UnableToResolveError(
fromModule, fromModule,
@ -453,22 +459,16 @@ class ResolutionRequest<TModule: Moduleish, TPackage: Packageish> {
const {name, type} = getAssetDataFromName(potentialModulePath, this._platforms); const {name, type} = getAssetDataFromName(potentialModulePath, this._platforms);
let pattern = name + '(@[\\d\\.]+x)?'; let pattern = '^' + name + '(@[\\d\\.]+x)?';
if (this._platform != null) { if (this._platform != null) {
pattern += '(\\.' + this._platform + ')?'; pattern += '(\\.' + this._platform + ')?';
} }
pattern += '\\.' + type; pattern += '\\.' + type + '$';
// Escape backslashes in the path to be able to use it in the regex const assetFiles = this._matchFiles(dirname, new RegExp(pattern));
if (path.sep === '\\') { // We arbitrarly grab the lowest, because scale selection will happen
dirname = dirname.replace(/\\/g, '\\\\'); // somewhere else. Always the lowest so that it's stable between builds.
} const assetFile = getArrayLowestItem(assetFiles);
// We arbitrarly grab the first one, because scale selection
// will happen somewhere
const [assetFile] = this._hasteFS.matchFiles(
new RegExp(dirname + '(\/|\\\\)' + pattern)
);
if (assetFile) { if (assetFile) {
return this._moduleCache.getAssetModule(assetFile); return this._moduleCache.getAssetModule(assetFile);
} }
@ -582,6 +582,19 @@ function isRelativeImport(filePath) {
return /^[.][.]?(?:[/]|$)/.test(filePath); return /^[.][.]?(?:[/]|$)/.test(filePath);
} }
function getArrayLowestItem(a: Array<string>): string | void {
if (a.length === 0) {
return undefined;
}
let lowest = a[0];
for (let i = 1; i < a.length; ++i) {
if (a[i] < lowest) {
lowest = a[i];
}
}
return lowest;
}
ResolutionRequest.emptyModule = require.resolve('./assets/empty-module.js'); ResolutionRequest.emptyModule = require.resolve('./assets/empty-module.js');
module.exports = ResolutionRequest; module.exports = ResolutionRequest;

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const path = require('path');
/**
* This is a way to find files quickly given a RegExp, in a specific directory.
* This is must faster than iterating over all the files and matching both
* directory and RegExp at the same time.
*
* This was first implemented to support finding assets fast, for which we know
* the directory, but we want to identify all variants (ex. @2x, @1x, for
* a picture's different definition levels).
*/
class FilesByDirNameIndex {
_filesByDirName: Map<string, Array<string>>;
constructor(allFilePaths: Array<string>) {
this._filesByDirName = new Map();
for (let i = 0; i < allFilePaths.length; ++i) {
const filePath = allFilePaths[i];
const dirName = path.dirname(filePath);
let dir = this._filesByDirName.get(dirName);
if (dir === undefined) {
dir = [];
this._filesByDirName.set(dirName, dir);
}
dir.push(path.basename(filePath));
}
}
match(dirName: string, pattern: RegExp): Array<string> {
const results = [];
const dir = this._filesByDirName.get(dirName);
if (dir === undefined) {
return [];
}
for (let i = 0; i < dir.length; ++i) {
if (pattern.test(dir[i])) {
results.push(path.join(dirName, dir[i]));
}
}
return results;
}
}
module.exports = FilesByDirNameIndex;

View File

@ -13,6 +13,7 @@
const Cache = require('./Cache'); const Cache = require('./Cache');
const DependencyGraphHelpers = require('./DependencyGraph/DependencyGraphHelpers'); const DependencyGraphHelpers = require('./DependencyGraph/DependencyGraphHelpers');
const FilesByDirNameIndex = require('./FilesByDirNameIndex');
const JestHasteMap = require('jest-haste-map'); const JestHasteMap = require('jest-haste-map');
const Module = require('./Module'); const Module = require('./Module');
const ModuleCache = require('./ModuleCache'); const ModuleCache = require('./ModuleCache');
@ -76,6 +77,7 @@ const JEST_HASTE_MAP_CACHE_BREAKER = 1;
class DependencyGraph extends EventEmitter { class DependencyGraph extends EventEmitter {
_opts: Options; _opts: Options;
_filesByDirNameIndex: FilesByDirNameIndex;
_haste: JestHasteMap; _haste: JestHasteMap;
_helpers: DependencyGraphHelpers; _helpers: DependencyGraphHelpers;
_moduleCache: ModuleCache; _moduleCache: ModuleCache;
@ -91,12 +93,14 @@ class DependencyGraph extends EventEmitter {
super(); super();
invariant(config.opts.maxWorkerCount >= 1, 'worker count must be greater or equal to 1'); invariant(config.opts.maxWorkerCount >= 1, 'worker count must be greater or equal to 1');
this._opts = config.opts; this._opts = config.opts;
this._filesByDirNameIndex = new FilesByDirNameIndex(config.initialHasteFS.getAllFiles());
this._haste = config.haste; this._haste = config.haste;
this._hasteFS = config.initialHasteFS; this._hasteFS = config.initialHasteFS;
this._moduleMap = config.initialModuleMap; this._moduleMap = config.initialModuleMap;
this._helpers = new DependencyGraphHelpers(this._opts); this._helpers = new DependencyGraphHelpers(this._opts);
this._haste.on('change', this._onHasteChange.bind(this)); this._haste.on('change', this._onHasteChange.bind(this));
this._moduleCache = this._createModuleCache(); this._moduleCache = this._createModuleCache();
(this: any)._matchFilesByDirAndPattern = this._matchFilesByDirAndPattern.bind(this);
} }
static _createHaste(opts: Options): JestHasteMap { static _createHaste(opts: Options): JestHasteMap {
@ -149,6 +153,7 @@ class DependencyGraph extends EventEmitter {
_onHasteChange({eventsQueue, hasteFS, moduleMap}) { _onHasteChange({eventsQueue, hasteFS, moduleMap}) {
this._hasteFS = hasteFS; this._hasteFS = hasteFS;
this._filesByDirNameIndex = new FilesByDirNameIndex(hasteFS.getAllFiles());
this._moduleMap = moduleMap; this._moduleMap = moduleMap;
eventsQueue.forEach(({type, filePath, stat}) => eventsQueue.forEach(({type, filePath, stat}) =>
this._moduleCache.processFileChange(type, filePath, stat) this._moduleCache.processFileChange(type, filePath, stat)
@ -226,6 +231,7 @@ class DependencyGraph extends EventEmitter {
extraNodeModules: this._opts.extraNodeModules, extraNodeModules: this._opts.extraNodeModules,
hasteFS: this._hasteFS, hasteFS: this._hasteFS,
helpers: this._helpers, helpers: this._helpers,
matchFiles: this._matchFilesByDirAndPattern,
moduleCache: this._moduleCache, moduleCache: this._moduleCache,
moduleMap: this._moduleMap, moduleMap: this._moduleMap,
platform, platform,
@ -243,6 +249,10 @@ class DependencyGraph extends EventEmitter {
}).then(() => response); }).then(() => response);
} }
_matchFilesByDirAndPattern(dirName: string, pattern: RegExp) {
return this._filesByDirNameIndex.match(dirName, pattern);
}
matchFilesByPattern(pattern: RegExp) { matchFilesByPattern(pattern: RegExp) {
return Promise.resolve(this._hasteFS.matchFiles(pattern)); return Promise.resolve(this._hasteFS.matchFiles(pattern));
} }