diff --git a/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart b/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart index 62c0bb92e6d..99eb26310fb 100644 --- a/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart +++ b/packages/devtools_app/lib/src/screens/memory/framework/memory_controller.dart @@ -21,6 +21,7 @@ import '../panes/chart/controller/chart_pane_controller.dart'; import '../panes/diff/controller/diff_pane_controller.dart'; import '../panes/profile/profile_pane_controller.dart'; import '../panes/tracing/tracing_pane_controller.dart'; +import '../shared/primitives/memory_utils.dart'; import 'offline_data/offline_data.dart'; /// Screen controller for the Memory screen. @@ -129,7 +130,7 @@ class MemoryController extends DevToolsScreenController chart = MemoryChartPaneController(data: offlineData?.chart ?? ChartData()); final rootPackage = isConnected - ? serviceConnection.serviceManager.rootInfoNow().package! + ? serviceConnection.serviceManager.rootInfoNow().rootPackagePrefix! : null; diff = diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart index cfc008b89fe..c46a531978c 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_diff.dart @@ -12,6 +12,7 @@ import '../../../../../shared/memory/classes.dart'; import '../../../../../shared/primitives/utils.dart'; import '../../../../../shared/table/table.dart'; import '../../../../../shared/table/table_data.dart'; +import '../../../shared/primitives/memory_utils.dart'; import '../../../shared/primitives/simple_elements.dart'; import '../../../shared/widgets/class_filter.dart'; import '../../../shared/widgets/shared_memory_widgets.dart'; @@ -53,7 +54,9 @@ class _ClassNameColumn extends ColumnData theClass: data.className, showCopyButton: isRowSelected, copyGaItem: gac.MemoryEvents.diffClassDiffCopy.name, - rootPackage: serviceConnection.serviceManager.rootInfoNow().package, + rootPackage: serviceConnection.serviceManager + .rootInfoNow() + .rootPackagePrefix, ); } diff --git a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart index 460ebf9d572..3e5b022da66 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/diff/widgets/classes_table_single.dart @@ -12,6 +12,7 @@ import '../../../../../shared/primitives/byte_utils.dart'; import '../../../../../shared/primitives/utils.dart'; import '../../../../../shared/table/table.dart'; import '../../../../../shared/table/table_data.dart'; +import '../../../shared/primitives/memory_utils.dart'; import '../../../shared/primitives/simple_elements.dart'; import '../../../shared/widgets/class_filter.dart'; import '../../../shared/widgets/shared_memory_widgets.dart'; @@ -50,7 +51,9 @@ class _ClassNameColumn extends ColumnData theClass: data.className, showCopyButton: isRowSelected, copyGaItem: gac.MemoryEvents.diffClassSingleCopy.name, - rootPackage: serviceConnection.serviceManager.rootInfoNow().package, + rootPackage: serviceConnection.serviceManager + .rootInfoNow() + .rootPackagePrefix, ); } diff --git a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart index 0465b9f6daf..fd274c065ff 100644 --- a/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart +++ b/packages/devtools_app/lib/src/screens/memory/panes/profile/profile_view.dart @@ -18,6 +18,7 @@ import '../../../../shared/table/table_controller.dart'; import '../../../../shared/table/table_data.dart'; import '../../../../shared/ui/common_widgets.dart'; import '../../shared/heap/class_filter.dart'; +import '../../shared/primitives/memory_utils.dart'; import '../../shared/primitives/simple_elements.dart'; import '../../shared/widgets/class_filter.dart'; import '../../shared/widgets/shared_memory_widgets.dart'; @@ -146,7 +147,9 @@ class _FieldClassNameColumn extends ColumnData theClass: data.heapClass, showCopyButton: isRowSelected, copyGaItem: gac.MemoryEvents.diffClassSingleCopy.name, - rootPackage: serviceConnection.serviceManager.rootInfoNow().package, + rootPackage: serviceConnection.serviceManager + .rootInfoNow() + .rootPackagePrefix, ); } diff --git a/packages/devtools_app/lib/src/screens/memory/shared/heap/sampler.dart b/packages/devtools_app/lib/src/screens/memory/shared/heap/sampler.dart index 36b72c1c638..f322719ceb3 100644 --- a/packages/devtools_app/lib/src/screens/memory/shared/heap/sampler.dart +++ b/packages/devtools_app/lib/src/screens/memory/shared/heap/sampler.dart @@ -13,6 +13,7 @@ import '../../../../shared/memory/classes.dart'; import '../../../../shared/memory/heap_data.dart'; import '../../../../shared/memory/heap_object.dart'; import '../../../../shared/utils/vm_utils.dart'; +import '../primitives/memory_utils.dart'; class _HeapObjects { _HeapObjects(this.objects, this.heap); @@ -106,7 +107,7 @@ class LiveClassSampler { bool get isEvalEnabled => heapClass.classType( - serviceConnection.serviceManager.rootInfoNow().package, + serviceConnection.serviceManager.rootInfoNow().rootPackagePrefix, ) != ClassType.runtime; diff --git a/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_utils.dart b/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_utils.dart index b14327f5aff..f3ebf7ccc5d 100644 --- a/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_utils.dart +++ b/packages/devtools_app/lib/src/screens/memory/shared/primitives/memory_utils.dart @@ -2,10 +2,43 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. +/// @docImport '../../../../shared/memory/class_name.dart'; +library; + +import 'package:devtools_app_shared/service.dart' show RootInfo; import 'package:vm_service/vm_service.dart'; import '../../../../shared/globals.dart'; +/// Memory screen specific information derived from the [RootInfo] of the +/// connected app. +extension RootInfoMemoryExtension on RootInfo { + /// The prefix of the URIs of the libraries that belong to the root package of + /// the connected app, or `null` if the root library is unknown. + /// + /// The Memory screen passes this prefix as `rootPackage` to + /// [HeapClassName.classType], which classifies a class as + /// [ClassType.rootPackage] (a "project" class) if the URI of its library + /// starts with the prefix. + /// + /// For the root library `package:my_app/main.dart`, the prefix is + /// `package:my_app`. + /// + /// Unlike [RootInfo.package], the prefix is not `null` when the root library + /// is not a `package:` URI. For example, the root library of a Dart CLI app + /// started with `dart run` is a `file:` URI like `file:///app/bin/main.dart`, + /// and the prefix is `file:`. Without a prefix, the classes defined in the + /// files of such an app would be classified as [ClassType.runtime], which + /// disables evaluating them in the live app. + String? get rootPackagePrefix { + final library = this.library; + if (library == null) return null; + + final slashIndex = library.indexOf('/'); + return slashIndex == -1 ? library : library.substring(0, slashIndex); + } +} + Future snapshotMemoryInSelectedIsolate() async { final isolate = serviceConnection.serviceManager.isolateManager.selectedIsolate.value; diff --git a/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart b/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart index e0282d61194..528cbdb5ae2 100644 --- a/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart +++ b/packages/devtools_app/test/screens/memory/framework/memory_controller_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/memory/shared/heap/class_filter.dart'; +import 'package:devtools_app_shared/service.dart' show RootInfo; import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:devtools_test/helpers.dart'; @@ -177,4 +178,26 @@ void main() { expect(scene.controller.diff.core.snapshots.value.length, 1); }); }); + + group('root package', () { + for (final (rootLibrary, expectedRootPackage) in const [ + ('package:my_app/main.dart', 'package:my_app'), + // The root library of a Dart CLI app started with `dart run` is a `file:` + // URI, not a `package:` URI. + ('file:///app/bin/main.dart', 'file:'), + ]) { + testWidgets( + 'is $expectedRootPackage when the root library is $rootLibrary', + (WidgetTester tester) async { + scene = MemoryDefaultScene(); + await scene.setUp( + classList: classList, + rootInfo: RootInfo(rootLibrary), + ); + + expect(scene.controller.trace!.rootPackage, expectedRootPackage); + }, + ); + } + }); } diff --git a/packages/devtools_app/test/screens/memory/shared/primitives/memory_utils_test.dart b/packages/devtools_app/test/screens/memory/shared/primitives/memory_utils_test.dart new file mode 100644 index 00000000000..b6dd9f03ee1 --- /dev/null +++ b/packages/devtools_app/test/screens/memory/shared/primitives/memory_utils_test.dart @@ -0,0 +1,58 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app/src/screens/memory/shared/primitives/memory_utils.dart'; +import 'package:devtools_app/src/shared/memory/class_name.dart'; +import 'package:devtools_app_shared/service.dart' show RootInfo; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('RootInfoMemoryExtension.rootPackagePrefix', () { + for (final (rootLibrary, expectedPrefix) in const [ + ('package:my_app/main.dart', 'package:my_app'), + ('package:my_app/src/nested/file.dart', 'package:my_app'), + ('package:my_app', 'package:my_app'), + // The root library of a Dart CLI app started with `dart run`. + ('file:///app/bin/main.dart', 'file:'), + ('org-dartlang-app:///lib/main.dart', 'org-dartlang-app:'), + (null, null), + ]) { + test('is $expectedPrefix for $rootLibrary', () { + expect(RootInfo(rootLibrary).rootPackagePrefix, expectedPrefix); + }); + } + + test('classifies classes of a file: root library as project classes', () { + const rootLibrary = 'file:///app/bin/main.dart'; + final heapClass = HeapClassName( + library: rootLibrary, + className: 'MyClass', + ); + + expect( + heapClass.classType(RootInfo(rootLibrary).rootPackagePrefix), + ClassType.rootPackage, + ); + }); + + test('keeps classes of other libraries out of the project', () { + final rootPrefix = RootInfo('package:my_app/main.dart').rootPackagePrefix; + + expect( + HeapClassName( + library: 'package:my_app/src/model.dart', + className: 'Model', + ).classType(rootPrefix), + ClassType.rootPackage, + ); + expect( + HeapClassName( + library: 'package:other_package/other.dart', + className: 'Other', + ).classType(rootPrefix), + ClassType.dependency, + ); + }); + }); +} diff --git a/packages/devtools_app/test/test_infra/scenes/memory/default.dart b/packages/devtools_app/test/test_infra/scenes/memory/default.dart index 58cf958300c..95a02461e27 100644 --- a/packages/devtools_app/test/test_infra/scenes/memory/default.dart +++ b/packages/devtools_app/test/test_infra/scenes/memory/default.dart @@ -7,6 +7,7 @@ import 'package:devtools_app/src/screens/memory/framework/memory_tabs.dart'; import 'package:devtools_app/src/screens/memory/panes/diff/controller/diff_pane_controller.dart'; import 'package:devtools_app/src/screens/memory/panes/profile/profile_pane_controller.dart'; import 'package:devtools_app/src/screens/memory/shared/heap/class_filter.dart'; +import 'package:devtools_app_shared/service.dart' show RootInfo; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_shared/devtools_shared.dart'; @@ -97,9 +98,11 @@ class MemoryDefaultScene extends Scene { /// /// [classList] will be returned by VmService.getClassList. /// [heapProviders] will be used to for heap snapshotting. + /// [rootInfo] will be returned by ServiceManager.rootInfoNow. Future setUp({ ClassList? classList, List? heapProviders, + RootInfo? rootInfo, }) async { heapProviders = heapProviders ?? MemoryDefaultSceneHeaps.all; @@ -129,6 +132,7 @@ class MemoryDefaultScene extends Scene { allocationData: allocationJson, classList: classList, ), + rootInfo: rootInfo, ); final app = fakeServiceConnection.serviceManager.connectedApp!; mockConnectedApp(app, isProfileBuild: true); diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index 49654528020..683bc2bdaf9 100644 --- a/packages/devtools_app_shared/CHANGELOG.md +++ b/packages/devtools_app_shared/CHANGELOG.md @@ -10,6 +10,9 @@ found in the LICENSE file or at https://developers.google.com/open-source/licens * Safely handle RPC errors and unexpected exceptions when calling service extensions in `ServiceExtensionManager`. * The minimum Dart SDK version is bumped to 3.11.0. * The minimum Flutter SDK version is bumped to 3.41.0. +* Fix `RootInfo.package` to be `null` when the root library is not a `package:` URI, + such as the `file:` URI of a Dart CLI app started with `dart run`. Previously it + was the scheme of the URI, such as `file:`, which is not a package. * Updates `devtools_shared` constraint to `^14.0.1`. ## 0.5.1 diff --git a/packages/devtools_app_shared/lib/src/service/isolate_state.dart b/packages/devtools_app_shared/lib/src/service/isolate_state.dart index d08e05e60c0..4f7a1077a4f 100644 --- a/packages/devtools_app_shared/lib/src/service/isolate_state.dart +++ b/packages/devtools_app_shared/lib/src/service/isolate_state.dart @@ -69,14 +69,29 @@ class IsolateState { } } +/// Information about the root library of an isolate of the connected app. +/// +/// The root library is the library that contains the `main` function of the +/// isolate. See also [Isolate.rootLib]. class RootInfo { RootInfo(this.library) : package = _libraryToPackage(library); + /// The URI of the root library, or `null` if it is unknown. + /// + /// This is not necessarily a `package:` URI. For example, the root library of + /// a Dart CLI app started with `dart run` is a `file:` URI like + /// `file:///app/bin/main.dart`. final String? library; + + /// The package that contains the root library, in the form `package:name`, or + /// `null` if it is unknown. + /// + /// This is `null` if [library] is `null` or is not a `package:` URI (for + /// example, a `file:` URI), since the name of the package is not known then. final String? package; static String? _libraryToPackage(String? library) { - if (library == null) return null; + if (library == null || !library.startsWith('package:')) return null; final slashIndex = library.indexOf('/'); if (slashIndex == -1) return library; return library.substring(0, slashIndex); diff --git a/packages/devtools_app_shared/test/service/isolate_state_test.dart b/packages/devtools_app_shared/test/service/isolate_state_test.dart new file mode 100644 index 00000000000..1fcbe6351f1 --- /dev/null +++ b/packages/devtools_app_shared/test/service/isolate_state_test.dart @@ -0,0 +1,45 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app_shared/src/service/isolate_state.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('RootInfo', () { + test('handles package URIs', () { + final info = RootInfo('package:my_package/main.dart'); + expect(info.library, 'package:my_package/main.dart'); + expect(info.package, 'package:my_package'); + }); + + test('handles package URIs without path', () { + final info = RootInfo('package:my_package'); + expect(info.library, 'package:my_package'); + expect(info.package, 'package:my_package'); + }); + + test('handles null library', () { + final info = RootInfo(null); + expect(info.library, isNull); + expect(info.package, isNull); + }); + + test('handles file URIs (non-package)', () { + final info = RootInfo( + 'file:///Users/viktor/Projects/tests/gui_1/gui_1_server/bin/main.dart', + ); + expect( + info.library, + 'file:///Users/viktor/Projects/tests/gui_1/gui_1_server/bin/main.dart', + ); + expect(info.package, isNull); + }); + + test('handles dart: URIs (non-package)', () { + final info = RootInfo('dart:core'); + expect(info.library, 'dart:core'); + expect(info.package, isNull); + }); + }); +} diff --git a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart index 5b948b568dd..bb2b821ce0d 100644 --- a/packages/devtools_test/lib/src/mocks/fake_service_manager.dart +++ b/packages/devtools_test/lib/src/mocks/fake_service_manager.dart @@ -28,6 +28,7 @@ class FakeServiceConnectionManager extends Fake List availableServices = const [], List availableLibraries = const [], String? rootLibrary, + RootInfo? rootInfo, }) { _serviceManager = FakeServiceManager( service: service, @@ -36,6 +37,7 @@ class FakeServiceConnectionManager extends Fake availableLibraries: availableLibraries, availableServices: availableServices, rootLibrary: rootLibrary, + rootInfo: rootInfo, ); for (final screen in ScreenMetaData.values) { final screenId = screen.id; @@ -92,9 +94,11 @@ class FakeServiceManager extends Fake this.onVmServiceOpened, Map? serviceExtensionResponses, String? rootLibrary, + RootInfo? rootInfo, bool hasConnection = true, }) : serviceExtensionResponses = serviceExtensionResponses ?? _defaultServiceExtensionResponses, + _rootInfo = rootInfo ?? RootInfo('package:myPackage/myPackage.dart'), _isolateManager = FakeIsolateManager(rootLibrary: rootLibrary) { this.service = service ?? createFakeService(); serviceUri = this.service!.wsUri; @@ -179,8 +183,15 @@ class FakeServiceManager extends Fake @override ConnectedApp? connectedApp = MockConnectedApp(); + /// The [RootInfo] returned by [rootInfoNow]. + /// + /// This is independent of the `rootLibrary` constructor argument, which only + /// configures the root library of the fake main isolate. Some tests depend on + /// this defaulting to a fixed `package:` root library. + final RootInfo _rootInfo; + @override - RootInfo rootInfoNow() => RootInfo('package:myPackage/myPackage.dart'); + RootInfo rootInfoNow() => _rootInfo; @override Future tryToDetectMainRootInfo() => Future.value(rootInfoNow());