-
-
Notifications
You must be signed in to change notification settings - Fork 929
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
Implement caching #2293
Implement caching #2293
Conversation
@sergesemashko Awesome, thanks for helping 👍 |
Looks like you did a great job researching the lessons we learned over at ESLint. I'm very excited to see this functionality land in stylelint. 👏 |
I have more free time this week to submit tests as well, I'm close |
UPDATE: I added tests for cache feature. Travis and appveyor are running out of memory in the very beginning, not sure why :/ On my local all tests are passing on node 7.5.0: @ntwb, @evilebottnawi @davidtheclark, any ideas or suggestions? |
@sergesemashko this is an exciting feature, thanks for working on it! When we first started using Jest we had some issues with CI failing due to running out of memory. It looks like that is what's causing the failures on this PR too. I've tried running your branch locally and noticed that the tests take significantly longer to run when compared to Here's the tests on Here's the tests on (Note that the failing tests will be different on each run) The |
@m-allanson, thanks for checking and pointing out to that. I'll investigate the reason. Definitely don't want to introduce any issues for testing :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sergesemashko 🥇 It's awesome that you are taking this on, and that you researched it so well. Interesting stuff! It's fun for me to learn new things reviewing this, and to think about some new problems.
Sorry for the delay reviewing: I knew I would need to find a chunk of time to read the code thoroughly and put effort into understanding the details of such a new feature.
I left several questions and comments. Looking forward to your response!
docs/user-guide/node-api.md
Outdated
@@ -77,6 +77,20 @@ If `true`, all disable comments (e.g. `/* stylelint-disable block-no-empty */`) | |||
|
|||
You can use this option to see what your linting results would be like without those exceptions. | |||
|
|||
## `cache` | |||
|
|||
Store the info about processed files in order to only operate on the changed ones. The cache is stored in `.stylelintcache` by default. Enabling this option can dramatically improve Stylelint's running time by ensuring that only changed files are linted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: we have been writing "stylelint" lowercase.
docs/user-guide/node-api.md
Outdated
|
||
Store the info about processed files in order to only operate on the changed ones. The cache is stored in `.stylelintcache` by default. Enabling this option can dramatically improve Stylelint's running time by ensuring that only changed files are linted. | ||
|
||
**Note:** If you run Stylelint with `--cache` and then run Stylelint without `--cache`, the `.stylelintcache` file will be deleted. This is necessary because the results of the lint might change and make `.stylelintcache` invalid. If you want to control when the cache file is deleted, then use `--cache-location` to specify an alternate location for the cache file. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a little confusing to me: does that mean that if I use --cache-location
, my cache file will become outdated when I run without --cache
, because it will not be deleted? I'm not sure I'd want this implicit behavior change solely because I specified a cache file location.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, cache file will be deleted when --cache-location
is not changing between runs with --cache
and without. Cache file won't be deleted for the following case:
stylelint *.scss --cache
- create cache file in default locationstylelint *.scss --cache-location /tmp
- won't delete cache file from default locationstylelint *.scss --cache
- reuse cache file from default location
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, thanks for clarifying. I think we should remove this sentence from the docs because it represents an edge-case that is fairly confusing. If people ask about it, we could add a note somewhere else, like the FAQ.
docs/user-guide/node-api.md
Outdated
|
||
Path to the cache location. Can be a file or a directory. If no location is specified, `.stylelintcache` will be used. In that case, the file will be created in the directory where the `stylelint` command is executed. | ||
|
||
If a directory is specified, a cache file will be created inside the specified folder. The name of the file will be based on the hash of the current working directory (CWD). e.g.: `.cache_hashOfCWD` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use a hash filename instead of simply adding .stylelintcache
to that directory?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will allow to reuse a single location for the caches from different projects and still receive the benefits of the cache. I'll add this sentence to the doc.
see eslint/eslint#4255 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good!
@@ -0,0 +1 @@ | |||
/* This will not cause a CSS syntax error */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean this will not cause a linting error? Those "invalid" files also will not cause CSS syntax errors, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, right, I'll update the wording
mockedFileCache.verify() | ||
}) | ||
}) | ||
afterEach(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please put this up by the beforeEach
? I find that helps force me to grasp the full context of each test upfront.
expect(fileCache._hashOfConfig).toBe(hashOfConfig) | ||
mockedFileEntryCache.verify() | ||
// restore mocked objects | ||
mockedFileEntryCache.restore() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of these two lines? Am I missing an assertion that they relate to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree, line 23 doesn't make sense. Line 24 restores file-entry-cache dependency for further use in case of new tests.
mockedFileEntryCache.restore() | ||
mock.stopAll() | ||
}) | ||
it("reconcile() stores hash to descriptor", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we need this test. Shouldn't we be able to run tests that determine whether FileCache
actually works as expected without caring about the implementation details? This seems like it's just testing implementation details, not the API of the FileCache
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FileCache.prototype.reconcile() wraps file-cache-entry
.reconcile() to add config hash before saving to every file entry. Just trying to make sure config hash is added on reconcile
getFileDescriptor: getFileDescriptorStub, | ||
}, | ||
_hashOfConfig: hashOfConfig, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of creating this mock fileCache
object, can't you use stub
to stub
the real getFileDescriptor
method on a real FileCache
instance? If so, wouldn't that would also avoid the need to awkwardly use call
below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, since I'm going to have more real things in tests, that would be the new way of testing hasFileChanged
expect(hasFileChanged.call(fileCache, invalidFile1)).toBe(true) | ||
expect(hasFileChanged.call(fileCache, invalidFile2)).toBe(true) | ||
}) | ||
it("file-cache-entry methods are called", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this matter for our tests? Aren't these implementation details?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just was trying to make sure the constructor is covered by tests. Maybe doesn't make sense when real FileCache is used elsewhere, I'll if it's still needed after update
const fixturesPath = path.join(__dirname, "fixtures") | ||
|
||
describe("standalone cache is enabled", () => { | ||
let mockedFileCache |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we mocking the file cache instead of testing the real thing? Couldn't we actually use the file cache in our tests, cleaning up after as needed, and then bypass some of the efforts below to test its internals?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I'll change to the real thing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to write tests isolating dependencies. Based on the comments I see that it makes sense to test real things in some cases.
- I'll update tests.
- and investigate performance bottleneck
lib/utils/getCacheFile.js
Outdated
return getCacheFileForDirectory() | ||
} | ||
|
||
return resolvedCacheFile |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense
expect(fileCache._hashOfConfig).toBe(hashOfConfig) | ||
mockedFileEntryCache.verify() | ||
// restore mocked objects | ||
mockedFileEntryCache.restore() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree, line 23 doesn't make sense. Line 24 restores file-entry-cache dependency for further use in case of new tests.
mockedFileEntryCache.restore() | ||
mock.stopAll() | ||
}) | ||
it("reconcile() stores hash to descriptor", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FileCache.prototype.reconcile() wraps file-cache-entry
.reconcile() to add config hash before saving to every file entry. Just trying to make sure config hash is added on reconcile
Awesome work @sergesemashko! |
What's in the latest update:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! I made a few copy edits directly, hope you don't mind. Left a couple of comments for you, also. I think we're pretty close.
const standalone = require("../standalone") | ||
const hash = require("../utils/hash") | ||
const fixturesPath = path.join(__dirname, "fixtures") | ||
const fsExtra = require("fs-extra") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather we avoid this new dependency and just use regular fs
. For copying you could use https://github.com/sindresorhus/cpy. Additionally, we should be able to avoid sync calls for all of this because Jest has good async/Promise support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, I'll try to avoid sync calls and minimize dependencies
expect(output.errored).toBe(true) | ||
// Ensure only changed files are linted | ||
const isValidFileLinted = !!output.results.find(file => file.source === validFile) | ||
const isInvalidFileLinted = !!output.results.find(file => file.source === changedFile) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should that be invalidFile
instead of changedFile
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized that changedFile is a confusing var name here, it should be something like newFileDest
. The idea here is to create new file so standalone should process both cached and uncached files.
// Ensure cache file doesn't contain invalid file | ||
const cachedFiles = fsExtra.readJsonSync(expectedCacheFilePath) | ||
expect(typeof cachedFiles[validFile] === "object").toBe(true) | ||
expect(typeof cachedFiles[changedFile] === "undefined").toBe(true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, wondering if this should be invalidFile
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll update the var as per comment
@jeddy3 I think we're close on this one. Do you want to give it a review? It's a pretty significant feature, so I want to make sure I'm not the only one who looks it over. |
@davidtheclark, thanks for the copy edits and feedback. |
@sergesemashko I'm afraid not. I try to ignore Appveyor. We have some Windows users in @stylelint/core. One guess: have you used |
@davidtheclark, good idea, I'll try normalized paths in tests. |
@davidtheclark, Here are the latest updates:
|
@davidtheclark @evilebottnawi @jeddy3, could you please take a look? |
@sergesemashko can you squash commits for easy review? |
bfce140
to
5ceec77
Compare
Fix flow annotations (stylelint#2293) Add test to cover cache implementation (stylelint#2293)
…elint#2293) Apply fixes regarding comments in pull request.
…nt#2293) - Refactored sync calls into promises. - Copy edits.
@sergesemashko Some questions:
|
@evilebottnawi, thanks for your feedback.
it's already accounted. Every cached entry has the hash of the config. If config changes all the files will be re-linted because hashes don't match. Checkout this line
This will allow to reuse a single location (let's say
Thanks, wasn't aware of this package. Looks like it may be helpful for any other cases and it looks much lighter that
Didn't quite get your concerns. Do you see potential issues with introducing |
903d7c5
to
e7b6ef3
Compare
Replace fs-promise by cpFile and pify Optimized pify usage Add cache flow param annotations (stylelint#2293)
02a4feb
to
bd9654d
Compare
Replace fs-promise by cpFile and pify Optimized pify usage Add cache flow param annotations (stylelint#2293) (cherry picked from commit e7b6ef3) Fix flow annotation for FileCache (cherry picked from commit f7a593b)
Replace fs-promise by cpFile and pify Optimized pify usage Add cache flow param annotations (stylelint#2293) (cherry picked from commit e7b6ef3) Fix flow annotation for FileCache (cherry picked from commit f7a593b)
79743b8
to
1dd4951
Compare
@sergesemashko Thanks for answer.
No i don't see problems, we should add SGTM for me 🥇 |
@evilebottnawi , I see. Frankly speaking, adding debug for all codebase sounds like a "big deal" for me 😃 I'm not yet that familiar with all the cases where it really should be added. |
@jeddy3 are you good with merging this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jeddy3 are you good with merging this?
Yeap. This is awesome!
(Sorry about the delay btw, I've only just got around catching up on stylelint after a very busy couple of months)
@sergesemashko Thanks! And welcome to the team :)
Added to changelog:
|
@sergesemashko I experienced the work of the cache and it's great! Only I noticed one strangeness: My npm script: |
@evilebottnawi, thanks for the input. |
@sergesemashko default command |
I was wondering why is
|
@olegskl can you create issue? |
@olegskl, thanks for reporting this. fileCache is initialized after if (!files) {
const absoluteCodeFilename = (codeFilename !== undefined && !path.isAbsolute(codeFilename))
? path.join(process.cwd(), codeFilename)
: codeFilename
return stylelint._lintSource({
code,
codeFilename: absoluteCodeFilename,
}).then(postcssResult => {
return stylelint._createStylelintResult(postcssResult)
}).catch(handleError).then(stylelintResult => {
return prepareReturnValue([stylelintResult])
})
} because this block is executed when raw styles are passed. So, there is no need to initialize However, looks like you found a bug, which wasn't covered by tests. @olegskl, could you please submit an issue? And I'll take a look at it. |
@sergesemashko here it is #2492, sorry for the late reply. |
#2270
UPDATED:
Tests are
in progressdone.I decided to submit a PR for initial feedback before going too deep into woods with tests.New dependencies:
DEBUG=stylelint:standalone stylelint "*.scss"
fs-extra
version to read, copy, remove files using promises in tests.Accounted for issues ESlint had with implementing cache:
cacheLocation
handles paths in windows style. (fixes #4285)@jeddy3, @davidtheclark, please, take a look