Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache option for searchers #43

Merged
merged 17 commits into from
Nov 18, 2023
Merged

Cache option for searchers #43

merged 17 commits into from
Nov 18, 2023

Conversation

antonk52
Copy link
Owner

@antonk52 antonk52 commented Nov 15, 2023

This PR introduces a new option cache(it should have been there from the start). The API and implementation is inspired by cosmiconfig. Similarities between lilconfig and cosmiconfig implementation:

  • Both use maps for storing caching results
  • The same public API to clear searchCache/loadCache/both
  • cache option is enabled by default
  • sync and async load methods use filepath argument as cache key (cosmiconfig)
  • sync and async search methods use path to a directory where lookup for search places will take place as cache key (cosmiconfig from is reassigned and called recursively in a closure)

I have added test cases for cache option enabled and disabled. Jest reports test coverage to be 100%. I am happy to add more tests if anyone can spot any missed cases.

Once approved I will merge this PR and release this as a new major version since this option is enabled by default. I consider this change in behaviour a breaking change. A few more changes will be merged prior to releasing a new version like dropping support for legacy node.js versions.

TODO

  • async implementation
  • sync implementation
  • test enabled option
  • test disabled option
  • add to readme
  • compare to cosmiconfig implementation
  • add searchers shape comparison in tests

fixes #42

Copy link

@pmdartus pmdartus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this PR is still in draft. But I thought that you might appreciate some early feedback.
Very excited about this feature landing. 👏

src/index.ts Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated
searchPath,
} of searchItems) {
if (cache) {
if (searchCache.has(searchPath)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If lilconfig is configured with multiple searchPlaces, this code will lookup for the same cache entry for each search place. In the case of Prettier, 14 different searchPlaces are passed.

Since Map.prototype.has is very efficient, it shouldn't make a big difference performance-wise. But it be ideal to avoid this.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using searchPath as a cache key, not filepath. On each iteration searchPath is the directory where we expect the config to be, not the full path to the config file. This keeps the code simpler and we will have less entries in the cache as a result. Example values from a loop iteration:

searchPath -> /foo/bar/baz
filepath -> /foo/bar/baz/prettier.config.js

I agree that the naming is rather confusing. I haven't came up with a better names.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification. I think my comment is still valid, here is a concrete example:

const resolver = lilconfig('config', {
  searchPlaces: ['package.json', '.prettierrc', '.prettierrc.json'],
});

resolver.search('./foo');

In the above example, getSearchItems would generate the following entries:

  • { searchPath: './foo', filepath: './foo/package.json' }
  • { searchPath: './foo', filepath: './foo/.prettierrc' }
  • { searchPath: './foo', filepath: './foo/.prettierrc.json' }
  • { searchPath: './', filepath: './package.json' }
  • { searchPath: './', filepath: './.prettierrc' }
  • { searchPath: './', filepath: './.prettierrc.json' }

Correct me if I am wrong but with the for loop iterating over the entries generated by getSearchItems, when cache is enabled, it appears to me that this code will do:

  • 3 consecutive cache lookup with: searchCache.get('./foo')
  • follow by, 3 consecutive cache lookup with: searchCache.get('./')

IMO, it's not a big deal performance-wise since the has check on a Map is not very expensive. But ideally, the code should do 1 lookup for each searchpath in the search cache.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for expanding on this. I've switched the look up for two nested loops. No more unnecessary look ups

@antonk52 antonk52 marked this pull request as ready for review November 15, 2023 22:26
src/index.ts Outdated
searchPath,
} of searchItems) {
if (cache) {
if (searchCache.has(searchPath)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification. I think my comment is still valid, here is a concrete example:

const resolver = lilconfig('config', {
  searchPlaces: ['package.json', '.prettierrc', '.prettierrc.json'],
});

resolver.search('./foo');

In the above example, getSearchItems would generate the following entries:

  • { searchPath: './foo', filepath: './foo/package.json' }
  • { searchPath: './foo', filepath: './foo/.prettierrc' }
  • { searchPath: './foo', filepath: './foo/.prettierrc.json' }
  • { searchPath: './', filepath: './package.json' }
  • { searchPath: './', filepath: './.prettierrc' }
  • { searchPath: './', filepath: './.prettierrc.json' }

Correct me if I am wrong but with the for loop iterating over the entries generated by getSearchItems, when cache is enabled, it appears to me that this code will do:

  • 3 consecutive cache lookup with: searchCache.get('./foo')
  • follow by, 3 consecutive cache lookup with: searchCache.get('./')

IMO, it's not a big deal performance-wise since the has check on a Map is not very expensive. But ideally, the code should do 1 lookup for each searchpath in the search cache.

src/index.ts Show resolved Hide resolved
src/index.ts Show resolved Hide resolved
@pmdartus
Copy link

A few more changes will be merged prior to releasing a new version like dropping support for legacy node.js versions.

As a side note, Prettier still supports Node 14 today.

I don't know what is the plan for Prettier to dropping Node 14 and Node 16 (both EOL). I know it's not ideal, but it would be great if lilconfig could still maintain backward compat with Node 14.

@antonk52
Copy link
Owner Author

I think this PR is ready to be merged. I ran a few benchmarks in feat/cache-bench branch. And made a couple of more small touches around getting from and updating the cache. Overall I am happy about the implementation and tests.

There is a small dip in test coverage because of an edge case that only occurs in nixos. I am not sure how this can be tested, so ignoring it for now.

While I am confident about basic benchmarks with set and map, the benchmarks with searches seem ludicrous to me. I might have messed up something with benchmark.js. I have no doubt about the new implementation being faster than the old one, but these numbers show too much of a difference to be believable.

Running "sync single search"
old x 959 ops/sec ±1.01% (93 runs sampled)
new x 31,080,130 ops/sec ±0.36% (97 runs sampled)
Fastest is new

@antonk52 antonk52 merged commit 6f815f4 into master Nov 18, 2023
5 checks passed
@antonk52 antonk52 deleted the feat/cache branch November 18, 2023 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adding caching support
2 participants