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

Compile the dart_dev run script for better performance #426

Merged
merged 12 commits into from
Apr 1, 2024

Conversation

alexandercampbell-wk
Copy link
Contributor

This PR adapted from a 2021 PR by @evanweible-wf: #363. See that PR for discussion and performance metrics.

My changes:

  • Merge conflicts resolved.
  • Most code review comments addressed.
  • Misc cleanup and rearrangement to bring this branch in line with since-merged refactors (most notably the refactor of path usage performed in Fixes for Windows compatibility #386; specifically 1ad8316).

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 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.

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.
@aviary3-wk
Copy link

aviary3-wk commented Mar 5, 2024

Security Insights

The 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
  • Watched keyword md5. in lib/src/executable.dart line(s) ['161'] added
  • Action Items

    • Obtain a security review; reviewer should pay special attention to insights listed above
    • Verify aviary.yaml coverage of security relevant code

    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.
    @greglittlefield-wf
    Copy link
    Contributor

    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.

    Copy link
    Contributor

    @greglittlefield-wf greglittlefield-wf left a 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 Show resolved Hide resolved
    lib/src/executable.dart Outdated Show resolved Hide resolved
    return;
    }

    final digest = md5.convert([
    Copy link
    Contributor

    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?

    Copy link
    Contributor

    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?

    Copy link
    Contributor

    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.

    Copy link
    Contributor Author

    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.

    Copy link
    Contributor Author

    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.

    lib/src/tools/clean_tool.dart Show resolved Hide resolved
    evanweible-wf
    evanweible-wf previously approved these changes Mar 18, 2024
    Copy link
    Contributor

    @evanweible-wf evanweible-wf left a 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.

    final pubSpecLock = loadYamlDocument(pubspecLockFile.readAsStringSync());
    return getDependencySources(pubSpecLock, packageNames)
    .values
    .every((source) => source == 'hosted');
    Copy link
    Contributor

    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.

    Copy link
    Contributor Author

    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.

    @alexandercampbell-wk
    Copy link
    Contributor Author

    @evanweible-wf @greglittlefield-wf this is ready for re-review at your convenience.

    @evanweible-wf
    Copy link
    Contributor

    QA +1

    • Installed this branch locally in a dart project
    • Verified that the run script is compiled and cached and subsequent runs do not recompile
    • Verified that the run script is recompiled when a change is detected

    @evanweible-wf
    Copy link
    Contributor

    +1 security

    • Use of md5 has no security impact; it is used only to compute a digest for the purposes of caching the run script

    @greglittlefield-wf greglittlefield-wf dismissed their stale review March 29, 2024 20:24

    Changes reviewed by Evan

    @alexandercampbell-wk
    Copy link
    Contributor Author

    @Workiva/release-management-pp

    Comment on lines +60 to +65
    final process = await Process.start(
    processArgs.first,
    [
    if (processArgs.length > 1) ...processArgs.sublist(1),
    ...args,
    ],
    Copy link
    Member

    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.

    Comment on lines +108 to +111
    [_paths.runExecutable, _paths.runExecutableDigest].forEach((p) {
    final f = File(p);
    if (f.existsSync()) f.deleteSync();
    });
    Copy link
    Member

    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

    Comment on lines +208 to +215
    final args = [
    'compile',
    'exe',
    _paths.runScript,
    '-o',
    _paths.runExecutable
    ];
    final result = Process.runSync(Platform.executable, args);
    Copy link
    Member

    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(" ")}');
    Copy link
    Member

    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')) {
    Copy link
    Member

    Choose a reason for hiding this comment

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

    Nice touch.

    Copy link
    Member

    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() {
    Copy link
    Member

    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.

    Comment on lines +200 to +227
    // 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}');
    }
    Copy link
    Member

    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());
    Copy link
    Member

    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.
    Copy link
    Member

    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. 😉

    Copy link
    Member

    @tomconnell-wf tomconnell-wf left a 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.

    @tomconnell-wf
    Copy link
    Member

    Security +1

    I don't see anything that can make dart_dev do something that it isn't expected to do.

    Copy link
    Contributor

    @rmconsole-wf rmconsole-wf left a comment

    Choose a reason for hiding this comment

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

    +1 from RM

    @rm-astro-wf rm-astro-wf merged commit 2ec310a into master Apr 1, 2024
    8 checks passed
    @rm-astro-wf rm-astro-wf deleted the compile_run_script_for_better_perf_followup branch April 1, 2024 19:20
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    8 participants