-
Notifications
You must be signed in to change notification settings - Fork 40
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
Compile the dart_dev
run script for better performance
#426
Compile the dart_dev
run script for better performance
#426
Conversation
After compilation, the startup time for subsequent `dart run dart_dev` commands should be significantly improved. This compiled exectuable is cached in `.dart_tool/dart_dev/` along with a digest that is used to determine when recompilation is needed. Packages with `tool/dart_dev/config.dart` files that import from their own package's source files will be opted out of this compilation optimization because there is no efficient way to track those imports for the purpose of detecting the need to recompile. This commit also adds a `clean` subcommand to `dart run dart_dev` that will remove the files in `.dart_tool/dart_dev/`, in case a bug is found in our compilation caching logic.
Security InsightsThe items listed below may not capture all security relevant changes. Before providing a security review, be sure to review the entire PR for security impact. (1) Security relevant changes were detected md5. in lib/src/executable.dart line(s) ['161'] addedAction Items
Questions or Comments? Reach out on Slack: #support-infosec. |
Also run `ddev format` to fix wrapping in executable.dart.
I think this will fix the CI failure on Windows specifically.
Thanks for reviving this @alexandercampbell-wk!! I'm super excited on about this performance optimization, and had been meaning to circle back to it for a while. At a glance these changes look good, but I can do a more thorough review later today. |
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.
A couple comments and some optional-to-address #nits
Tested a little bit locally and seems to work great!
lib/src/executable.dart
Outdated
return; | ||
} | ||
|
||
final digest = md5.convert([ |
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.
If we're allowing imports of other packages, I think we should include something in the digest that causes recompilation when dependencies change. E.g., the contents of .dart_tool/package_config.json
.
Oh, it looks ilke that was present in the previous PR: https://github.com/Workiva/dart_dev/pull/363/files#diff-16bd3bf901e6560bcc074b4d1fdc6750f4c1ab19007f4d6f0d1acfa0989b9f08R160
Could we reinstate that?
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.
Also, there's this related comment about skipping caching for mutable dependencies: https://github.com/Workiva/dart_dev/pull/363/files#r753702320
Feels like it'd be worth addressing; @evanweible-wf what do you think?
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.
Yeah +1 to both of these comments - even if it's rare for mutable deps to be imported and used in ddev config, it'd be a very confusing issue to debug, so it'd be nice to cover 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.
I have added the contents of .dart_tool/package_config.json
to the md5 digest here.
Handling caching for mutable dependencies, based on package_config
, pubspec.yaml
, and pubspec.lock
, sounds like a more complex task. I'll have to follow up on this next week. Thank you both for the careful review.
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.
@evanweible-wf @greglittlefield-wf I have addressed this comment. It took me an hour or so to implement but it is working well now.
Sadly, the main repo where I was interested in using this precompilation system does have a path
dependency in the imports for the config file, meaning that this safety change now forces recompilation on every run :/
I can think of some workarounds (tree walking + statting for timestamps?), but nothing quick. I think this PR is probably good to go for now and we could try to think of a more sophisticated solution in the future.
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 one minor thing, but LGTM.
lib/src/executable.dart
Outdated
final pubSpecLock = loadYamlDocument(pubspecLockFile.readAsStringSync()); | ||
return getDependencySources(pubSpecLock, packageNames) | ||
.values | ||
.every((source) => source == 'hosted'); |
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 just realized this makes it hard to test the behavior locally because any custom tool/dart_dev/config.dart
will always be importing dart_dev
from a local or git source. We should be able to consider git
sources immutable, too, since the package config file included in the digest below will include the resolved ref of each git source.
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've updated this line to consider 'hosted'
and 'git'
immutable.
@evanweible-wf @greglittlefield-wf this is ready for re-review at your convenience. |
QA +1
|
+1 security
|
@Workiva/release-management-pp |
final process = await Process.start( | ||
processArgs.first, | ||
[ | ||
if (processArgs.length > 1) ...processArgs.sublist(1), | ||
...args, | ||
], |
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.
Security review: parameterized shelling out is good.
[_paths.runExecutable, _paths.runExecutableDigest].forEach((p) { | ||
final f = File(p); | ||
if (f.existsSync()) f.deleteSync(); | ||
}); |
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.
Security review: The files to delete look like they are statically defined, so this couldn't be used to delete something unexpected: https://github.com/Workiva/dart_dev/pull/426/files#diff-6e5f65bcac0e9f8da735ef58d443983df38419788614afe4a860adebb5016f5cR35-R37
final args = [ | ||
'compile', | ||
'exe', | ||
_paths.runScript, | ||
'-o', | ||
_paths.runExecutable | ||
]; | ||
final result = Process.runSync(Platform.executable, args); |
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.
Security review: parameterized shelling 👍
// user that dart_dev should still work. | ||
log.warning( | ||
'Could not compile run script; dart_dev will continue without precompilation.'); | ||
log.fine('CMD: ${Platform.executable} ${args.join(" ")}'); |
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.
Security review: afaik, logging out the args given isn't problematic on cli.
import 'utils/version.dart'; | ||
|
||
class DartDevRunner extends CommandRunner<int> { | ||
DartDevRunner(Map<String, DevTool> commands) | ||
: super('dart_dev', 'Dart tool runner.') { | ||
// For backwards-compatibility, only add the `clean` command if it doesn't | ||
// conflict with any configured command. | ||
if (!commands.containsKey('clean')) { |
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.
Nice touch.
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 file is starting to have a lot of responsibilities. I could have seen the new digest-related functions in a separate file. 🤷
|
||
/// Return null iff it is not possible to account for all | ||
/// recompilation-necessitating factors in the digest. | ||
String? _computeRunScriptDigest() { |
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.
It would have been nice to split this function up to call a config getter, configImports validator, and digest encoder function so it was easier to digest.
// Digest is missing or outdated, so we (re-)compile. | ||
final logMessage = runExecutable.existsSync() | ||
? 'Recompiling run script (digest changed)' | ||
: 'Compiling run script'; | ||
logTimedSync(log, logMessage, () { | ||
// Delete the previous executable and digest so that if we hit a failure | ||
// trying to compile, we don't leave the outdated one in place. | ||
_deleteRunExecutableAndDigest(); | ||
final args = [ | ||
'compile', | ||
'exe', | ||
_paths.runScript, | ||
'-o', | ||
_paths.runExecutable | ||
]; | ||
final result = Process.runSync(Platform.executable, args); | ||
if (result.exitCode == 0) { | ||
// Compilation succeeded. Write the new digest, as well. | ||
runExecutableDigest.writeAsStringSync(encodedDigest!); | ||
} else { | ||
// Compilation failed. Dump some logs for debugging, but note to the | ||
// user that dart_dev should still work. | ||
log.warning( | ||
'Could not compile run script; dart_dev will continue without precompilation.'); | ||
log.fine('CMD: ${Platform.executable} ${args.join(" ")}'); | ||
log.fine('STDOUT:\n${result.stdout}'); | ||
log.fine('STDERR:\n${result.stderr}'); | ||
} |
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.
It would have been nice to split this out into a separate function. 🤷
|
||
@override | ||
FutureOr<int?> run([DevToolExecutionContext? context]) { | ||
final cache = Directory(DartDevPaths().cache()); |
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.
Security review: hardcoded to delete .dart-tool/dart_dev
|
||
/// Return the contents of the enquoted portion of the import statements in the | ||
/// file. Not 100% accurate, since we use regular expressions instead of the | ||
/// Dart AST to extract the imports. |
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.
pfft, not that Dart's AST parsing would be 100% accurate either. 😉
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.
To a dart_dev layperson familiar with the problem you're solving, this looks reasonable.
Security +1I don't see anything that can make dart_dev do something that it isn't expected to do. |
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.
+1 from RM
This PR adapted from a 2021 PR by @evanweible-wf: #363. See that PR for discussion and performance metrics.
My changes:
Functionality changes described below.
After compilation, the startup time for subsequent
dart run dart_dev
commands should be significantly improved.This compiled exectuable is cached in
.dart_tool/dart_dev/
along with a digest that is used to determine when recompilation is needed.Packages with
tool/dart_dev/config.dart
files that import from their own package's source files will be opted out of this compilation optimization because there is no efficient way to track those imports for the purpose of detecting the need to recompile.This commit also adds a
clean
subcommand todart run dart_dev
that will remove the files in.dart_tool/dart_dev/
, in case a bug is found in our compilation caching logic.