Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -53,7 +54,9 @@ class _ClassNameColumn extends ColumnData<DiffClassData>
theClass: data.className,
showCopyButton: isRowSelected,
copyGaItem: gac.MemoryEvents.diffClassDiffCopy.name,
rootPackage: serviceConnection.serviceManager.rootInfoNow().package,
rootPackage: serviceConnection.serviceManager
.rootInfoNow()
.rootPackagePrefix,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,7 +51,9 @@ class _ClassNameColumn extends ColumnData<SingleClassData>
theClass: data.className,
showCopyButton: isRowSelected,
copyGaItem: gac.MemoryEvents.diffClassSingleCopy.name,
rootPackage: serviceConnection.serviceManager.rootInfoNow().package,
rootPackage: serviceConnection.serviceManager
.rootInfoNow()
.rootPackagePrefix,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -146,7 +147,9 @@ class _FieldClassNameColumn extends ColumnData<ProfileRecord>
theClass: data.heapClass,
showCopyButton: isRowSelected,
copyGaItem: gac.MemoryEvents.diffClassSingleCopy.name,
rootPackage: serviceConnection.serviceManager.rootInfoNow().package,
rootPackage: serviceConnection.serviceManager
.rootInfoNow()
.rootPackagePrefix,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -106,7 +107,7 @@ class LiveClassSampler {

bool get isEvalEnabled =>
heapClass.classType(
serviceConnection.serviceManager.rootInfoNow().package,
serviceConnection.serviceManager.rootInfoNow().rootPackagePrefix,
) !=
ClassType.runtime;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HeapSnapshotGraph?> snapshotMemoryInSelectedIsolate() async {
final isolate =
serviceConnection.serviceManager.isolateManager.selectedIsolate.value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
},
);
}
});
}
Original file line number Diff line number Diff line change
@@ -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,
);
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<void> setUp({
ClassList? classList,
List<HeapProvider>? heapProviders,
RootInfo? rootInfo,
}) async {
heapProviders = heapProviders ?? MemoryDefaultSceneHeaps.all;

Expand Down Expand Up @@ -129,6 +132,7 @@ class MemoryDefaultScene extends Scene {
allocationData: allocationJson,
classList: classList,
),
rootInfo: rootInfo,
);
final app = fakeServiceConnection.serviceManager.connectedApp!;
mockConnectedApp(app, isProfileBuild: true);
Expand Down
3 changes: 3 additions & 0 deletions packages/devtools_app_shared/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
}
13 changes: 12 additions & 1 deletion packages/devtools_test/lib/src/mocks/fake_service_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class FakeServiceConnectionManager extends Fake
List<String> availableServices = const [],
List<String> availableLibraries = const [],
String? rootLibrary,
RootInfo? rootInfo,
}) {
_serviceManager = FakeServiceManager(
service: service,
Expand All @@ -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;
Expand Down Expand Up @@ -92,9 +94,11 @@ class FakeServiceManager extends Fake
this.onVmServiceOpened,
Map<String, Response>? 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;
Expand Down Expand Up @@ -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<RootInfo?> tryToDetectMainRootInfo() => Future.value(rootInfoNow());
Expand Down
Loading