Skip to content
This repository has been archived by the owner on Jun 10, 2024. It is now read-only.

Commit

Permalink
fix: Improve caching to improve performance (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
nzakas committed Sep 21, 2022
1 parent 86ce02c commit 8a7e8ab
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 48 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,13 @@ A few things to keep in mind:
* The config array caches configs, so subsequent calls to `getConfig()` with the same filename will return in a fast lookup rather than another calculation.
* A config will only be generated if the filename matches an entry in a `files` key. A config will not be generated without matching a `files` key (configs without a `files` key are only applied when another config with a `files` key is applied; configs without `files` are never applied on their own).

## Caching Mechanisms

Each `ConfigArray` aggressively caches configuration objects to avoid unnecessary work. This caching occurs in two ways:

1. **File-based Caching.** For each filename that is passed into a method, the resulting config is cached against that filename so you're always guaranteed to get the same object returned from `getConfig()` whenever you pass the same filename in.
2. **Index-based Caching.** Whenever a config is calculated, the config elements that were used to create the config are also cached. So if a given filename matches elements 1, 5, and 7, the resulting config is cached with a key of `1,5,7`. That way, if another file is passed that matches the same config elements, the result is already known and doesn't have to be recalculated. That means two files that match all the same elements will return the same config from `getConfig()`.

## Acknowledgements

The design of this project was influenced by feedback on the ESLint RFC, and incorporates ideas from:
Expand Down
110 changes: 63 additions & 47 deletions src/config-array.js
Original file line number Diff line number Diff line change
Expand Up @@ -315,56 +315,55 @@ export class ConfigArray extends Array {
* @param {Array<string>} [options.configTypes] List of config types supported.
*/
constructor(configs, {
basePath = '',
normalized = false,
schema: customSchema,
extraConfigTypes = []
} = {}
basePath = '',
normalized = false,
schema: customSchema,
extraConfigTypes = []
} = {}
) {
super();

/**
* Tracks if the array has been normalized.
* @property isNormalized
* @type boolean
* @private
*/
* Tracks if the array has been normalized.
* @property isNormalized
* @type boolean
* @private
*/
this[ConfigArraySymbol.isNormalized] = normalized;

/**
* The schema used for validating and merging configs.
* @property schema
* @type ObjectSchema
* @private
*/
this[ConfigArraySymbol.schema] = new ObjectSchema({
...customSchema,
...baseSchema
});
* The schema used for validating and merging configs.
* @property schema
* @type ObjectSchema
* @private
*/
this[ConfigArraySymbol.schema] = new ObjectSchema(
Object.assign({}, customSchema, baseSchema)
);

/**
* The path of the config file that this array was loaded from.
* This is used to calculate filename matches.
* @property basePath
* @type string
*/
* The path of the config file that this array was loaded from.
* This is used to calculate filename matches.
* @property basePath
* @type string
*/
this.basePath = basePath;

assertExtraConfigTypes(extraConfigTypes);

/**
* The supported config types.
* @property configTypes
* @type Array<string>
*/
* The supported config types.
* @property configTypes
* @type Array<string>
*/
this.extraConfigTypes = Object.freeze([...extraConfigTypes]);

/**
* A cache to store calculated configs for faster repeat lookup.
* @property configCache
* @type Map
* @private
*/
* A cache to store calculated configs for faster repeat lookup.
* @property configCache
* @type Map
* @private
*/
this[ConfigArraySymbol.configCache] = new Map();

// init cache
Expand Down Expand Up @@ -600,63 +599,80 @@ export class ConfigArray extends Array {

assertNormalized(this);

// first check the cache to avoid duplicate work
let finalConfig = this[ConfigArraySymbol.configCache].get(filePath);
const cache = this[ConfigArraySymbol.configCache];

// first check the cache for a filename match to avoid duplicate work
let finalConfig = cache.get(filePath);

if (finalConfig) {
return finalConfig;
}

// next check to see if the file should be ignored

// TODO: Maybe move elsewhere?
const relativeFilePath = path.relative(this.basePath, filePath);

if (shouldIgnoreFilePath(this.ignores, filePath, relativeFilePath)) {
debug(`Ignoring ${filePath}`);

// cache and return result - finalConfig is undefined at this point
this[ConfigArraySymbol.configCache].set(filePath, finalConfig);
cache.set(filePath, finalConfig);
return finalConfig;
}

// filePath isn't automatically ignored, so try to construct config

const matchingConfigs = [];
const matchingConfigIndices = [];
let matchFound = false;

for (const config of this) {
this.forEach((config, index) => {

if (!config.files) {
debug(`Universal config found for ${filePath}`);
matchingConfigs.push(config);
continue;
matchingConfigIndices.push(index);
return;
}

if (pathMatches(filePath, this.basePath, config)) {
debug(`Matching config found for ${filePath}`);
matchingConfigs.push(config);
matchingConfigIndices.push(index);
matchFound = true;
continue;
return;
}
}

});

// if matching both files and ignores, there will be no config to create
if (!matchFound) {
debug(`No matching configs found for ${filePath}`);

// cache and return result - finalConfig is undefined at this point
this[ConfigArraySymbol.configCache].set(filePath, finalConfig);
cache.set(filePath, finalConfig);
return finalConfig;
}

// check to see if there is a config cached by indices
finalConfig = cache.get(matchingConfigIndices.toString());

if (finalConfig) {

// also store for filename for faster lookup next time
cache.set(filePath, finalConfig);

return finalConfig;
}

// otherwise construct the config

finalConfig = matchingConfigs.reduce((result, config) => {
return this[ConfigArraySymbol.schema].merge(result, config);
finalConfig = matchingConfigIndices.reduce((result, index) => {
return this[ConfigArraySymbol.schema].merge(result, this[index]);
}, {}, this);

finalConfig = this[ConfigArraySymbol.finalizeConfig](finalConfig);

this[ConfigArraySymbol.configCache].set(filePath, finalConfig);
cache.set(filePath, finalConfig);
cache.set(matchingConfigIndices.toString(), finalConfig);

return finalConfig;
}
Expand Down
12 changes: 11 additions & 1 deletion tests/config-array.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ describe('ConfigArray', () => {
expect(config.defs.css).to.be.false;
});

it('should return the same config when called with the same filename twice', () => {
it('should return the same config when called with the same filename twice (caching)', () => {
const filename = path.resolve(basePath, 'foo.js');

const config1 = configs.getConfig(filename);
Expand All @@ -660,6 +660,16 @@ describe('ConfigArray', () => {
expect(config1).to.equal(config2);
});

it('should return the same config when called with two filenames that match the same configs (caching)', () => {
const filename1 = path.resolve(basePath, 'foo1.js');
const filename2 = path.resolve(basePath, 'foo2.js');

const config1 = configs.getConfig(filename1);
const config2 = configs.getConfig(filename2);

expect(config1).to.equal(config2);
});

it('should return empty config when called with ignored node_modules filename', () => {
const filename = path.resolve(basePath, 'node_modules/foo.js');
const config = configs.getConfig(filename);
Expand Down

0 comments on commit 8a7e8ab

Please sign in to comment.