parallelize lute lint#1088
Conversation
|
This is a pretty large change and difficult to review at the moment. It seems like this is currently a refactor combined with the functional changes to parallelize linting. If that's the case, could I ask that you split out the refactor part into a separate PR to make it easier to review? |
Sure! Some of the refactor is tightly coupled to the constraints of using |
@skberkeley refactor up here, i'll rebase and clean up this branch once that is merged |
Separating refactor from parallelization change, as per [this request](#1088 (comment)). This PR moves business logic from `lute lint` into a separate `lintCore` module, providing a clear pathway to re-use logic without duplication when we add parallelization to `lute lint`. Changes are entirely non-functional.
skberkeley
left a comment
There was a problem hiding this comment.
this change feels quite risky to me; can you gate it behind a command line flag?
| rulePath = rulePath, | ||
| noDefaults = noDefaults, | ||
| configPath = passedConfigVal, | ||
| verbose = VERBOSE, |
There was a problem hiding this comment.
why are we passing these things around in a table?
There was a problem hiding this comment.
also why does every worker need to do initialize?
There was a problem hiding this comment.
We can't pass functions as args to the separate VM instance, so the best workaround I know of is re-initializing based on the location of relevant files (rules and config). If there's a better alternative, I'm all ears
Passing the args as a table is just a stylistic choice
There was a problem hiding this comment.
i see, it sounds like we should tease apart the parts of initialize which load the rules and the parts that load the config, and do the config loading before spawning the workers then
creating and reading from tables is more costly, and this isn't an option table, so we should pass the arguments around unwrapped
There was a problem hiding this comment.
Re passing as tables:
Sounds good, will change.
Re only initializing rules:
I think we still need to initialize both, since rule configs have arbitrary typing; it's totally possible, for instance, that a custom rule has an option that expects a function.
It looks like there's an issue up regarding support for passing all primitive types through the VM barrier, so we likely won't need initialize in the long-run. I can add a comment calling this out
| while curr ~= nil do | ||
| if hasIgnores then | ||
| local inputFilePathString = pathLib.format(curr) | ||
|
|
||
| if | ||
| parseIgnores.isIgnored( | ||
| ignoreData, | ||
| inputFilePathString, | ||
| fs.type(inputFilePathString) == "dir" | ||
| ) | ||
| then | ||
| curr = walker() | ||
| continue | ||
| end | ||
| end | ||
|
|
||
| local success, res = pcall(function() | ||
| return worker.lintFile({ | ||
| file = pathLib.format(curr :: pathLib.Path), | ||
| autofix = job.autoFixEnabled, | ||
| verbose = VERBOSE, | ||
| }) | ||
| end) | ||
| if success then | ||
| allViolations[curr] = res :: { types.LintViolation } | ||
| else | ||
| print(`(worker) Error linting file '{curr}': {res}`) | ||
| end | ||
|
|
||
| curr = walker() | ||
| end | ||
| curJob = lintNext() |
There was a problem hiding this comment.
this feels like a strange way to balance tasks across workers; if we give one worker a folder with many files and subfolders, we'll just end up waiting for that one. can you add a comment explaining that we don't have thread safe data structures so this is kind of the best we can do?
There was a problem hiding this comment.
Yea that's fair; I also explored keeping the inputFilePaths walking sequential, queueing jobs as files were walked, and having workers pull from the queue, but found it was empirically slower. The scenario you call out definitely makes sense in theory but also seems impossible given that getSourceFiles returns exclusively files and no directories. I kept the fs walk logic because that is how lintPaths worked previously, but maybe it's worth dropping 🤷
There was a problem hiding this comment.
I'd recommend scanning files and then dispatching them in groups afterwards. see what lute test/ the format PR does.
we should probably abstract this worker pool soon so we can share it between test, lint, transform, format, etc. it's all very similar
There was a problem hiding this comment.
Hi! I'll put up my draft for this sometime this week. When that happens I can refactor this code during my PR :)
Yes, my only concern is duplicating test logic as I like the safety of having every test case exercise the parallel codepath |
…age + update lint docs
| local inputFilePathString = pathLib.format(curr) | ||
| if not PARALLEL then | ||
| for _, inputPath in inputFiles do | ||
| local walker = fs.walk(inputPath, { recursive = true }) |
There was a problem hiding this comment.
I removed fs walking in the parallel path since we identified it's redundant given how getSourceFiles works. Kept it in non-parallel to keep changes as minimal as possible, but maybe worth removing too? Will defer to @skberkeley
There was a problem hiding this comment.
Yeah probably worth refactoring in a separate PR?
skberkeley
left a comment
There was a problem hiding this comment.
Looks good, thanks for taking my feedback into consideration! I'm going to ask @~Vighnesh-V to take a look too since he understands the VM/task library the best
| local inputFilePathString = pathLib.format(curr) | ||
| if not PARALLEL then | ||
| for _, inputPath in inputFiles do | ||
| local walker = fs.walk(inputPath, { recursive = true }) |
There was a problem hiding this comment.
Yeah probably worth refactoring in a separate PR?
|
|
||
| curr = walker() | ||
| end | ||
| end |
There was a problem hiding this comment.
if we early return here we don't need to wrap the parallel part in an else block
There was a problem hiding this comment.
I'm going to be a bit annoying and ask that we extract the code for 'running' lints into its own file. Probably something that exposes runSequential and runParallel as part of its export surface.
|
Oh, also maybe worth updating our CI to run |
| ### `-p, --parallel` | ||
|
|
||
| Lint files in parallel instead of sequentially. |
There was a problem hiding this comment.
why isn't this just the default? why wouldn't you want this? tests already run them in parallel by default
There was a problem hiding this comment.
I agree with Nick here. This should be parallel by default. Can you add the inverse (-s, --sequentially) instead, so users can opt in to determinism?
I think a flag means that no one will use it. lute should be fast by default, if we're not confident enough to have it on by default, why are we confident enough to add it behind a CLI flag that people are much less likely to use? |
| ### `-p, --parallel` | ||
|
|
||
| Lint files in parallel instead of sequentially. |
There was a problem hiding this comment.
I agree with Nick here. This should be parallel by default. Can you add the inverse (-s, --sequentially) instead, so users can opt in to determinism?
|
|
||
| curr = walker() | ||
| end | ||
| end |
There was a problem hiding this comment.
I'm going to be a bit annoying and ask that we extract the code for 'running' lints into its own file. Probably something that exposes runSequential and runParallel as part of its export surface.
| if | ||
| parseIgnores.isIgnored(ignoreData, inputFilePathString, fs.type(inputFilePathString) == "dir") |
There was a problem hiding this comment.
is this actually what stylua does if the condition is too long?
| task.spawn(function() | ||
| xpcall(function() | ||
| local worker = vm.create("@self/worker") | ||
| -- Initialize will be UNNECESSARY once we can pass functions to separate VM |
There was a problem hiding this comment.
oh, serializing functions across vm bodies. Could I suggest instead that we add support for adding arguments to vm.create calls, and the module can initialize state based on that?
There was a problem hiding this comment.
e.g. vm.create(.., rulesPath, noDefaults, passedConfigVal, VERBOSE)?
There was a problem hiding this comment.
That makes sense. Probably out of the scope of this PR though? Happy to handle in a separate PR and update this accordingly
| VERBOSE = args:has("verbose") | ||
| PARALLEL = args:has("parallel") |
There was a problem hiding this comment.
Capital case is usually used for constants. Can we stick these options in a table and then read them in here please?
There was a problem hiding this comment.
As in have a module-scoped table like { verbose: boolean, parallel: boolean, }? If so, why is that better than just changing the casing?
| passedConfigVal, | ||
| autofixEnabled, | ||
| ignoreData | ||
| ) -- LUAUFIX: type refinement on line 305 should mean that cast on inputFiles isn't needed |
There was a problem hiding this comment.
Isn't this file only 282 lines? Where does this point to now?
There was a problem hiding this comment.
Good question, from a quick look at commit history it looks like this comment's been bouncing around for a while. Maybe we just remove it
| fs.removeDirectory(modulePath, { recursive = true }) | ||
| end) | ||
|
|
||
| suite:case("luteLintParallelMultipleInputFiles", function(assert) |
There was a problem hiding this comment.
Can these be expressed as a snapshot test?
There was a problem hiding this comment.
If we're going to default to parallel, I'll swap these cases for --sequential. We can use snapshots for those
There was a problem hiding this comment.
On second thought, it doesn't really make sense to have dedicated snapshot tests (or explicit tests) for sequential linting. For the sake of safety I'd prefer if all test cases exercised both sequential and parallel code paths. This may be a bit redundant, but casts the widest net for catching regressions
…ential arg instead of parallel
…r to always test both parallel and sequential
Distribute walking input paths and linting files across workers. Number of workers capped by the minimum of available system threads and # of lintee files.
side note: it'd be reallyyyy sweet if we had documentation on the
vmlib and how to use it, as it took me a while to figure out what it's constraints were and how to work around them. If i'm missing something, pls lmk 😄Testing
All tests passing.
Also manually sanity checked on some local repos with the script in the collapsable below. Verified that every linted file had same the # of violations when linted with old / new build. Seeing pretty big improvements.
Comparing 5/08 nightly release (default) against release build from this branch (parallel)
Test script