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

Only scan the file that got changed and thus prevent build_runner from scanning unnecessary files #3448

Closed
simplenotezy opened this issue Jan 30, 2023 · 19 comments

Comments

@simplenotezy
Copy link

simplenotezy commented Jan 30, 2023

Question

Is there any way to only have it run the build_runner if an actual file was changed that requires the build_runner?

Problem: Changing this in my example widget, will take 10 seconds for the build_runner to process:

import 'package:my_app/common/exceptions/api_exceptions.dart';
import 'package:my_app/common/widgets/common/animated_loader.dart';
import 'package:my_app/common/widgets/common/error_handler.dart';
import 'package:my_app/common/widgets/test_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class AsyncValueWidget<T> extends StatelessWidget {

  const AsyncValueWidget({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
-    return SizedBox();
+    return Container();
}

Details

I know it's been mentioned here: #2928 and many other places. I did configure the generate_for in build.yaml

targets:
  $default:
    builders:
      mobx_codegen:mobx_generator:
        generate_for:
          - lib/legacy/**/*.dart
      auto_route_generator:autoRouteGenerator:
        generate_for:
          - lib/features/router/router.dart
      json_serializable:
        generate_for:
          - lib/common/domain/*.dart
          - lib/**/domain/*.dart
        options:
          explicit_to_json: true
      freezed:
        options:
          # https://pub.dev/packages/freezed#generating-code
        generate_for:
          - lib/**/domain/*.dart
          - lib/common/exceptions/api_exceptions.dart
          - lib/common/api/paginated_response.dart
      riverpod_generator:
        generate_for:
          - lib/**/application/*.dart
          - lib/**/*controller.dart
          - lib/**/data/*.dart
          - lib/**/*provider.dart
          - lib/common/api/dio.dart
          - lib/common/util/share_invite_link.dart
          - lib/common/util/easter_egg/easter_egg.dart

However, I have an issue where if I save a widget file, e.g.

lib/common/widgets/async_value_widget.dart

import 'package:my_app/common/exceptions/api_exceptions.dart';
import 'package:my_app/common/widgets/common/animated_loader.dart';
import 'package:my_app/common/widgets/common/error_handler.dart';
import 'package:my_app/common/widgets/test_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class AsyncValueWidget<T> extends StatelessWidget {

  const AsyncValueWidget({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox();
}

Then I'll see the following output in the build_runner:

[INFO] Build:Succeeded after 9.8s with 183 outputs (188 actions)

[INFO] Watch:------------------------------------------------------------------------

[INFO] Watch:Starting Build

[INFO] Build:Updating asset graph...
[INFO] Build:Updating asset graph completed, took 21ms

[INFO] Build:Running build...
[FINE] freezed on lib/features/profile/domain/profile_input.dart:Running FreezedGenerator
[FINE] freezed on lib/features/compliments/domain/profile_compliment.dart:Running FreezedGenerator
[FINE] freezed on lib/features/match/domain/match.dart:Running FreezedGenerator
[FINE] freezed on lib/features/compliments/domain/poll_user.dart:Running FreezedGenerator
[FINE] freezed on lib/features/invite_friends/domain/phone_number_invite.dart:Running FreezedGenerator
[FINE] freezed on lib/features/swipe/domain/swipe_page_state.dart:Running FreezedGenerator
[FINE] freezed on lib/features/compliments/domain/compliment.dart:Running FreezedGenerator
[FINE] freezed on lib/features/chat/domain/chat.dart:Running FreezedGenerator
[FINE] freezed on lib/features/message/domain/message.dart:Running FreezedGenerator
[FINE] freezed on lib/features/chat/domain/chat_user.dart:Running FreezedGenerator
[FINE] freezed on lib/features/compliments/domain/compliment_poll.dart:Running FreezedGenerator
[FINE] freezed on lib/features/profile/domain/profile.dart:Running FreezedGenerator
[FINE] freezed on lib/features/address/domain/auto_complete_address_input.dart:Running FreezedGenerator
[FINE] freezed on lib/features/compliments/domain/poll_game.dart:Running FreezedGenerator
[FINE] freezed on lib/features/profile/domain/relevant_profile.dart:Running FreezedGenerator
[FINE] freezed on lib/features/chat/domain/chat_page_state.dart:Running FreezedGenerator
[FINE] freezed on lib/features/chat/domain/chat_page_response.dart:Running FreezedGenerator
[FINE] freezed on lib/features/chat/domain/filters.dart:Running FreezedGenerator
[FINE] json_serializable on lib/features/chat/domain/mutable_chat.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/chat/domain/mutable_chat.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/compliments/domain/poll_game.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/compliments/domain/poll_game.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/compliments/domain/profile_compliment.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/compliments/domain/profile_compliment.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/compliments/domain/compliment.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/compliments/domain/compliment.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/compliments/domain/poll_user.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/compliments/domain/poll_user.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/match/domain/match.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/match/domain/match.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/invite_friends/domain/phone_number_invite.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/invite_friends/domain/phone_number_invite.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/profile/domain/profile.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/profile/domain/profile.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/chat/domain/chat_user.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/chat/domain/chat_user.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/message/domain/message.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/message/domain/message.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/address/domain/auto_complete_address_input.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/address/domain/auto_complete_address_input.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/chat/domain/chat.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/chat/domain/chat.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/profile/domain/profile_input.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/profile/domain/profile_input.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/compliments/domain/compliment_poll.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/compliments/domain/compliment_poll.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/swipe/domain/swipe_page_state.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/swipe/domain/swipe_page_state.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/profile/domain/relevant_profile.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/profile/domain/relevant_profile.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/chat/domain/filters.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/chat/domain/filters.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/chat/domain/chat_page_response.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/chat/domain/chat_page_response.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] json_serializable on lib/features/chat/domain/chat_page_state.dart:Running JsonSerializableGenerator - 1 of 2
[FINE] json_serializable on lib/features/chat/domain/chat_page_state.dart:Running JsonLiteralGenerator - 2 of 2
[FINE] riverpod_generator on lib/features/reporting/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/friend/application/invites.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/friend/application/suggestions.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/friend/application/friends.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/friend/data/suggestions_repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/friend/application/sent_invites.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/friend/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/compliments/application/profile_compliments.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/friend/data/invites_repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/compliments/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/compliments/application/top_compliments.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/compliments/application/poll.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/local_storage/data/local_storage_repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/local_storage/application/local_storage_service.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/compliments/application/can_see_compliments.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/reporting/application/reporting.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/match/application/provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/match/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/invite_friends/application/invite_progress.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/invite_friends/application/invite_link.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/chat/application/filters.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/chat/application/provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/chat/application/message_provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/chat/application/list_page_provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/chat/application/empty_list_provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/chat/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/chat/application/non_empty_list_provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/chat/presentation/conversation/controller.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/auth/application/auth.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/auth/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/auth/presentation/login/controller.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/notification/application/provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/notification/application/token_provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/notification/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/notification/application/notification_options.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/location/application/provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/location/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/contacts/application/contacts.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/contacts/application/inviteable_contacts.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/contacts/application/invited_contacts.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/contacts/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/swipe/application/provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/swipe/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/swipe/presentation/controller.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/user/application/user.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/user/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/permissions/application/notifications_permission.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/permissions/application/location_permission.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/alerts/application/alerts.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/profile/application/username_search.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/profile/application/edit_profile.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/profile/application/profiles.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/address/application/address.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/address/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/profile/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/profile/application/image_uploader.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/invite-link/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/place/application/places.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/interests/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/interests/application/provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/features/place/data/repository.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/common/util/share_invite_link.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/common/api/dio.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/common/api/graphql_provider.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/common/application/app_service.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/common/application/waiting_list.dart:Running RiverpodGenerator
[FINE] riverpod_generator on lib/common/application/dynamic_link_provider.dart:Running RiverpodGenerator
[INFO] Heartbeat:6.9s elapsed, 133/136 actions completed.
[INFO] Heartbeat:8.0s elapsed, 168/168 actions completed.
[FINE] auto_route_generator:autoRouteGenerator on lib/features/router/router.dart:Running AutoRouteGenerator
[INFO] Build:Running build completed, took 9.0s

[INFO] Build:Caching finalized dependency graph...
[INFO] Build:Caching finalized dependency graph completed, took 142ms

[INFO] Build:Succeeded after 9.2s with 183 outputs (188 actions)

Meaning it takes around 10 seconds when saving a file, where no generated code should be regenerated.

It appears that the runner is running quite quickly through the files, as they have no changes, but that still sums up to roughly 10 seconds.

Dart SDK Version:

2.18.6

What package(s) from this repo you are using, and the version:

2.1.11

What builder(s) you are using (or writing yourself). Try to give a short summary of what they do.

  • riverpod_generator
  • auto_route_generator
  • json_serializable
  • freezed
@jakemac53
Copy link
Contributor

build_runner already does dependency tracking in order to figure out which files it should run on based on a given change.

The issue is that for any codegen which does analysis, we have to add a dependency on all transitively imported files, because even changes in transitive libraries could affect the current library (through inference and exposed types). Code generators can also reach the full transitive set.

We also can't differentiate between API changes and method body changes today, because we do analysis from source and not summary files. This gives you access to the full AST (including method bodies).

The best things you can do are use generate_for to ensure your builders are only running on the files they need to, and then maintain as small of a set of imports as possible in the files which codegen does run on so they are invalidated less often.

@davidmartos96
Copy link

@jakemac53

and then maintain as small of a set of imports as possible in the files which codegen does run on so they are invalidated less often

I've recently encountered with the same behavior as the OP, where a small change in an "unrelated" file would rerun the code generation for many files.
Limiting the number of imports has definitely helped. Unfortunately, in large projects it can get very hard to limit the number of imports and it's very dependent on the type of code gen. If it's something like state-management like Riverpod or MobX, this transitive dependencies can quickly grow because it can touch many parts.

I was wondering if this would also apply for the macros in metaprogramming or if it would use a different mechanism to invalidate less often. If it would be the same, I'm worried about how easy it would be to reach a point where a large portion of a big project is invalidated and builds get noticeable slower.

@jakemac53
Copy link
Contributor

I was wondering if this would also apply for the macros in metaprogramming or if it would use a different mechanism to invalidate less often. If it would be the same, I'm worried about how easy it would be to reach a point where a large portion of a big project is invalidated and builds get noticeable slower.

The feature is carefully designed to enable much finer invalidation yes - but in practice it will depend on how much this is optimized by the implementations.

Macros can only access the "api" of their dependencies, so for instance white space edits and edits to method bodies would not typically necessitate a re-run of macros in down stream dependencies. Type inference does complicate things a bit here though.

The actual introspection API is also quite fine grained which would allow for further dependency tracking, if implementations decide it is worth the effort (and memory) to do such fine grained tracking.

@davidmartos96
Copy link

@jakemac53 Thank you for the insight

@simplenotezy
Copy link
Author

Thanks for the help @jakemac53 - can you point to any material/documentation in terms of how build runner can be optimised?

Currently we're a bit stuck, every time we save a single model/widget file (even one that does not need to be regenerated) it takes ~47 seconds on a M1 Pro, before the builder is done.

I've tried as much as possible to limit using generate_for, e.g.:

      json_serializable:
        generate_for:
          - lib/common/domain/*.dart

However, it still scans way too many files.

And by the way, is it on the roadmap to somehow optimise this behaviour? E.g. coming from React world, I'm used to super quick hot-reloads, even in very large projects, when saving a single file, and I would love to see something similar for Dart, as development experience is seriously hindered for us currently.

@jakemac53
Copy link
Contributor

https://github.com/dart-lang/build/blob/master/docs/measuring_performance.md is the best place to start to understand where the time is going.

Long term the goal is to integrate codegen into the compiler via macros which is how we will improve this significantly.

If things are invalidating more than you expect it is most likely due to the import structure of your app. Any Dart file could be affected in arbitrary ways based on its full transitive imports, so the more specific your imports are the better (this applies to both build_runner and general hot reload).

@simplenotezy
Copy link
Author

Thanks for that Jake! So when you say:

the more specific your imports are the better

Do you mean making sure code is separated into smaller files?

@jakemac53
Copy link
Contributor

It is more about having fewer transitive imports - so called "convenience imports" in particular can lead to major pain. Those are imports which just export a whole bunch of other libraries, so you can import one thing and get everything.

For small packages this is OK but for entire apps it means every library depends on the entire app, which will cause major scaling problems.

@jakemac53
Copy link
Contributor

If you are seeing files be regenerated that seem unrelated to the file you changed, it is almost certainly because of these types of import structures. A file must be regenerated if any transitive import is changed, whether it is directly referenced in the file or not. Changes to that file can still affect the analysis result indirectly.

@simplenotezy
Copy link
Author

I see.. Well we usually have one file per class/widget. But I guess in terms of optimising the build runner, if we had two classes in the same file:

  • Class A
  • Class B

Where Class B depends on Class A, it wouldn't make any difference if we moved Class B into a separate file, since class B would have to import class A.

Is that correctly understood?

@jakemac53
Copy link
Contributor

Correct, if there is a dependency between them then it is actually probably more efficient to keep them in a single library, to an extent. But I wouldn't recommend going very far down that path.

The exact behavior here may differ between build_runner and regular hot reload (ie: frontend_server). In particular build_runner is purely file contents based and will invalidate based on even whitespace changes, while frontend_server is at least a bit smarter then that, and there is ongoing effort there to make it smarter.

@jakemac53
Copy link
Contributor

The larger value thing is not doing the package:my_app/my_app.dart import though, where that thing effectively adds a transitive dep on the entire app.

@simplenotezy
Copy link
Author

Thanks a lot for your help Jake. Highly appreciated. Really looking forward to when build_runner is optimised - think it will have a huge impact on productivity

@mikklfr
Copy link

mikklfr commented May 3, 2023

A shoutout for this nice package that mitigates a bit the issue for now: https://github.com/jyotirmoy-paul/cached_build_runner

@simplenotezy could you give it a try with your code sample? I have something similar on my end and it works quite well.

Note: I had to do a build_runner build first to get cached_build_runner working.

@jakemac53
Copy link
Contributor

From a quick glance it looks like cached_build_runner is not taking transitive dependencies into account - that might be ok for a lot of cases but you may run into situations where it doesn't work. Depends a lot on the builder being used and the particular type of change in the transitive dep. So use at your own risk.

@jakemac53
Copy link
Contributor

Fwiw, you could also just make a different Resolver implementation that does the same thing (doesn't track transitive deps). While not safe/hermetic, it would probably give you much faster builds, and most of the time it probably does work out OK. I don't think it's something we can/should support directly but I have no problem with a third party implementation that is "unsafe" in this way. It wouldn't be super trivial to slot that resolver implementation in though (you would have to manually make a build script with build_runner_core).

Possibly we could support a build.yaml option to automatically slot in a specified resolver implementation though (probably given a Uri to import, and constructor to invoke).

@simplenotezy
Copy link
Author

simplenotezy commented May 3, 2023

Thanks I'll check out that package.

For now our biggest development hurdle with Dart is the massive slowdown we experience mainly due to build_runner working 90% of the time (rebuilds take way too long!), so I really hope this is something Dart will look into soon

@mikklfr
Copy link

mikklfr commented May 3, 2023

Thanks for the explanations, I better understand the limits now.

As you said, it could work on some specific situations and not on others.
I feel it's acceptable for the code pasted on the issue where you do minor edits and/or if you managed to split enough your codebase and want that 20sec delay reduced to zero.

Will keep in mind limitations with transitives deps ;
I would not use it on continuous integration builds but only for specific dev purpose and would advise to clean cache often.

Still glad to see that the Flutter community is really reactive on those topics 👍

@simplenotezy
Copy link
Author

Hey @mikklfr thanks for the shoutout. I have implemented it in our code now, and will report back (here or in the repo) with my findings

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

No branches or pull requests

4 participants