webdev icon indicating copy to clipboard operation
webdev copied to clipboard

[DWDS] Can't set breakpoint in file with circular dependency

Open elliette opened this issue 3 years ago • 8 comments

See https://github.com/flutter/flutter/issues/106727 for details.

How to reproduce:

  • Download the minimal example
  • Rename lib/main_dev.dart to main.dart
  • Follow instructions to debug DWDS with flutter_tools using the example app
  • Try to set a breakpoint on line 21 of account_presentation/controllers/sign_in_controller.dart

Note:

  • When the import that introduces a circular dependency (package:router/router.dart) is commented out, the moduleForSourcemethod correctly returns packages/account_presentation/controllers/sign_in_controller.dart.
  • With the import is included, it incorrectly returns packages/home/pages/welcome_page.dart.

elliette avatar Jul 18 '22 17:07 elliette

Looked at this a bit more. With the circular dependency, account_presentation/controllers/sign_in_controller.dart gets listed as a library for the packages/home/pages/welcome_page.dart module.

ModuleMetadata for packages/home/pages/welcome_page.dart:

With circular dependency
{
   "version":1.0.2,
   "name":"packages/home/pages/welcome_page.dart",
   "closureName":"load__packages__home__pages__welcome_page_dart",
   "sourceMapUri":"/packages/home/pages/welcome_page.dart.lib.js.map",
   "moduleUri":"/packages/home/pages/welcome_page.dart.lib.js",
   "libraries":[
      {
         "name":"welcome_page",
         "importUri":"package":"home/pages/welcome_page.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"unknown_page",
         "importUri":"package":"home/pages/unknown_page.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"splash_page",
         "importUri":"package":"home/pages/splash_page.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"main_layout_page",
         "importUri":"package":"home/layouts/main_layout_page.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"unknown_controller",
         "importUri":"package":"home/controllers/unknown_controller.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"splash_controller",
         "importUri":"package":"home/controllers/splash_controller.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"sign_in_page",
         "importUri":"package":"account_presentation/pages/sign_in_page.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"sign_in_controller",
         "importUri":"package":"account_presentation/controllers/sign_in_controller.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"sign_in_binding",
         "importUri":"package":"account_presentation/bindings/sign_in_binding.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"account_presentation_library",
         "importUri":"package":"account_presentation/account_presentation_library.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"router",
         "importUri":"package":"router/router.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"router_library",
         "importUri":"package":"router/router_library.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"main_layout_controller",
         "importUri":"package":"home/controllers/main_layout_controller.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"welcome_binding",
         "importUri":"package":"home/bindings/welcome_binding.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"unknown_binding",
         "importUri":"package":"home/bindings/unknown_binding.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"splash_binding",
         "importUri":"package":"home/bindings/splash_binding.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"home_library",
         "importUri":"package":"home/home_library.dart",
         "partUris":[
            
         ]
      }
   ],
   "soundNullSafety":true
}
Without circular dependency
{
   "version":1.0.2,
   "name":"packages/home/pages/welcome_page.dart",
   "closureName":"load__packages__home__pages__welcome_page_dart",
   "sourceMapUri":"/packages/home/pages/welcome_page.dart.lib.js.map",
   "moduleUri":"/packages/home/pages/welcome_page.dart.lib.js",
   "libraries":[
      {
         "name":"welcome_page",
         "importUri":"package":"home/pages/welcome_page.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"unknown_page",
         "importUri":"package":"home/pages/unknown_page.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"splash_page",
         "importUri":"package":"home/pages/splash_page.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"main_layout_page",
         "importUri":"package":"home/layouts/main_layout_page.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"unknown_controller",
         "importUri":"package":"home/controllers/unknown_controller.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"splash_controller",
         "importUri":"package":"home/controllers/splash_controller.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"router",
         "importUri":"package":"router/router.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"router_library",
         "importUri":"package":"router/router_library.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"main_layout_controller",
         "importUri":"package":"home/controllers/main_layout_controller.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"welcome_binding",
         "importUri":"package":"home/bindings/welcome_binding.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"unknown_binding",
         "importUri":"package":"home/bindings/unknown_binding.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"splash_binding",
         "importUri":"package":"home/bindings/splash_binding.dart",
         "partUris":[
            
         ]
      },
      {
         "name":"home_library",
         "importUri":"package":"home/home_library.dart",
         "partUris":[
            
         ]
      }
   ],
   "soundNullSafety":true
}

Adding @annagrin who has more context. How should ModuleMetadata / breakpoints handle circular dependencies?

elliette avatar Jul 18 '22 23:07 elliette

Circular dependencies result in JS modules that include all JS code for the dart included, so the module change is expected. Not sure what causes the issue, will look into it.

annagrin avatar Jul 19 '22 16:07 annagrin

Hi @annagrin, do you have any news about this? Thanks in advance!

jkydevelopment avatar Aug 05 '22 06:08 jkydevelopment

Investigated this - the problem is caused by disagreements between serving paths in dwds and relative paths in source maps:

Example

source directory tree

<current directory>
  a/
     lib/
       a.dart
     pubspec.yaml 
  b/
     lib/
       b.dart
     pubspec.yaml

where

  • package:a/a.dart imports package:b/b.dart
  • package:b/b.dart imports package:a/a.dart

DDC output

  • Module: packages/a/a.lib.js that contains code for both packages

  • Source map: packages/a/a.lib.js.map that contains paths relative to directory a/lib (where .js file is physically located):

    • a.dart
    • ../../b/lib/b.dart (meaningless relative to the serving path packages/a/a.lib.js - it should be ../b/b.dart to resolve correctly).

We can solve this by making relative paths in modules uris and relative paths in source maps agree, ie making both match either the actual directory structure or the structure of module uris (which are created from package: urls I believe)

This needs some experimentation and thinking, so not a trivial bug fix unfortunately. I suspect the fix should be in the frontend server since it writes out module metadata (that contains module uris) and source maps (that contain relative source paths), or flutter tools (that reads everything from the frontend server and starts dwds to serve the files).

@jkydevelopment To work around the issue, is it possible to break the circular dependency by either putting all the mutual dependent code in one package, or pulling the code both need to a third package and making the two depend on it instead?

annagrin avatar Aug 06 '22 01:08 annagrin

@annagrin thank you for your response. I have just tried it by moving some common packages to a new common package, which now contains core, locator and router.

In this case it's not possible to fix the circular dependency because of the dependency injection:

  • locator folder (now contained inside the new common package) uses get_it, so it needs to import some other packages to register each factory or singleton (ie account_domain).
  • In the other hand any other package which uses locator (ie account_domain) also needs to inject some elements which are retrieved from locator, which is inside common package, so the circular dependency it's always present.

The only way I could fix this issue is by only using the main lib folder and not creating any other package, but all the libraries would be contained in the main pubspec.yaml, and it would not be possible to apply a clean architecture to the project.

Any ideas about how could I structure this to avoid the circular dependency?

jkydevelopment avatar Aug 06 '22 10:08 jkydevelopment

I am working on the solution but not sure when it is going to be available (really depends where the change needs to be made, I am experimenting with a few ideas).

As for the package dependencies, looks like the account_domain is special because it creates a circular dependency. Can the part of account_domain (the part that uses locator and common) be moved to the common package so common does not need to depend on account_domain anymore? Another option is to unify common and account_domain into one package, if this makes more sense for the project.

annagrin avatar Aug 08 '22 17:08 annagrin

Proposed fix

make server paths follow directory structure:packages/<package_directory>/<path from package root to the file on disk>

Fix plan

  • Frontend server and expression compiler worker (DDC):
    • update the module uris in metadata files to match the scheme above, under a new debugger-module-uris flag
  • Dwds:
    • provide a way to map package: uris to server paths, ie a new class PackageUriMapper
    • pass the debugger-module-uris to the expression compiler worker
  • flutter tools:
    • use PackageUriMapper dwds API to resolve package: uris in WebAssetServer
    • pass the debugger-module-uris to the frontend server
  • cleanup:
    • set debugger-module-uris to true by default
    • after all the changes propagated to dart and flutter stable versions
      • remove passing of the flag by tools
      • remove the flag

annagrin avatar Aug 15 '22 18:08 annagrin

We should verify whether or not this is still an issue.

bkonyi avatar Nov 06 '24 18:11 bkonyi

@annagrin @bkonyi We can confirm this is still an issue on flutter 3.32. With the release of pub workspaces, we decided to transition our growing codebase to a modular project structure, with feature packages just as @jkydevelopment described. First, we couldn't debug at all cause of https://github.com/dart-lang/webdev/issues/2575. Today, still a lot of our features cannot be debugged on web. I finally found this old issue now, which describes exactly what we experience and also explains why some breakpoints can be set and others not. This is the error:

addBreakpointWithScriptUri: (102) The VM is unable to add a breakpoint bp/3302#40:0 at the specified line or function: (3302:40:0):  cannot find Dart location.

edhom avatar Aug 05 '25 09:08 edhom

@srujzs @elliette Is there anything we could do to support resolving this issue?

edhom avatar Sep 09 '25 09:09 edhom

Hi, we are refactoring some code in our repository and we introduced circular dependencies between packages to speed up the work. But by doing so we hit this issue in all of our Flutter web apps, so yes this is still an issue.

letsar avatar Sep 12 '25 15:09 letsar

Update: The changes that were landed in 2022 but never enabled do fix this issue by updating the directory layout of the files at serve time to match what is encoded in the source maps. Unfortunately, those changes also introduce new problems related to source maps and stack traces.

There are a lot of places throughout the Dart web dev infrastructure that assume a very specific server directory layout. Changing that layout will require staging more changes and plumbing more flags to roll it out. There are some longstanding goals to holistically redesign how we produce source maps and stack traces but that is a much larger task. My goal is to find a solution that targets just this problem that will be faster to roll out.

I've been exploring some potential fixes to resolve this issue and I have some ideas to work with. I'll be away from work for a few weeks but I'm planning on taking on this task when I return.

nshahan avatar Sep 30 '25 21:09 nshahan

I landed a fix for this issue in the Dart SDK https://github.com/dart-lang/sdk/commit/1c0c66f680f53d0a2b7d1c933fe244c0978beeaa. It has been rolled into the Flutter SDK and is now available on the main channel.

nshahan avatar Oct 28 '25 16:10 nshahan

~~The fix has also been published in the latest Flutter beta release: 3.38.0-0.2.pre if you want to try it there.~~

I'm wrong about that. I'll look into getting the fix in.

nshahan avatar Oct 30 '25 18:10 nshahan