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 @@ -65,7 +65,8 @@ extension type _UserAgentSpecificMemoryBreakdownAttributionElement._(JSObject _)
@JS()
extension type _UserAgentSpecificMemoryBreakdownAttributionContainerElement._(
JSObject _
) implements JSObject {
)
implements JSObject {
external String get id;

external String get url;
Expand Down
4 changes: 2 additions & 2 deletions packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ TODO: Remove this section if there are not any updates.

## CPU profiler updates

TODO: Remove this section if there are not any updates.
- Fixed an issue where DevTools would auto-select the test runner isolate instead of the test suite isolate when connecting to a test run, causing CPU profiling and other tools to show no data (#9747). [#9747](https://github.com/flutter/devtools/pull/9747)

## Memory updates

Expand Down Expand Up @@ -69,4 +69,4 @@ TODO: Remove this section if there are not any updates.
## Full commit history

To find a complete list of changes in this release, check out the
[DevTools git log](https://github.com/flutter/devtools/tree/v2.57.0).
[DevTools git log](https://github.com/flutter/devtools/tree/v2.57.0).
126 changes: 107 additions & 19 deletions packages/devtools_app_shared/lib/src/service/isolate_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -166,20 +166,26 @@ final class IsolateManager with DisposerMixin {
!event.isolate!.isSystemIsolate!) {
await _registerIsolate(event.isolate!);
_isolateCreatedController.add(event.isolate);
// TODO(jacobr): we assume the first isolate started is the main isolate
// but that may not always be a safe assumption.
if (_mainIsolate.value == null) {

// Recompute whenever a new isolate starts so test connections can move
// from the runner isolate to the user test-suite isolate when available.
final previousMain = _mainIsolate.value;
final computedMain = await _computeMainIsolate();
if (computedMain != null) {
_mainIsolate.value = computedMain;
} else if (_mainIsolate.value == null) {
_mainIsolate.value = event.isolate;
if (_shouldReselectMainIsolate) {
// Assume the main isolate has come back up after a hot restart, so
// select it.
_shouldReselectMainIsolate = false;
_setSelectedIsolate(event.isolate);
}
}

if (_selectedIsolate.value == null) {
_setSelectedIsolate(event.isolate);
if (_mainIsolate.value != null &&
(_shouldReselectMainIsolate ||
_selectedIsolate.value == null ||
_selectedIsolate.value == previousMain)) {
// If the previous main exited and returned (hot restart), or if the
// selected isolate was previously the main isolate, update selection to
// the newly computed main isolate.
_shouldReselectMainIsolate = false;
_setSelectedIsolate(_mainIsolate.value);
}
} else if (event.kind == EventKind.kServiceExtensionAdded) {
// Check to see if there is a new isolate.
Expand Down Expand Up @@ -223,25 +229,107 @@ final class IsolateManager with DisposerMixin {

final service = _service;
for (final isolateState in _isolateStates.values) {
if (_selectedIsolate.value == null) {
final isolate = await isolateState.isolate;
if (service != _service) return null;
for (final extensionName in isolate?.extensionRPCs ?? <String>[]) {
if (extensions.isFlutterExtension(extensionName)) {
return isolateState.isolateRef;
}
final isolate = await isolateState.isolate;
if (service != _service) return null;
for (final extensionName in isolate?.extensionRPCs ?? <String>[]) {
if (extensions.isFlutterExtension(extensionName)) {
return isolateState.isolateRef;
}
}
}

final ref = _isolateStates.keys.firstWhereOrNull((IsolateRef ref) {
// 'foo.dart:main()'
return ref.name!.contains(':main(');
return ref.name?.contains(':main(') ?? false;
});

if (ref == null) {
final rootLibraryTestSuiteRef =
await _findTestSuiteByRootLibrary(service);
if (rootLibraryTestSuiteRef != null) return rootLibraryTestSuiteRef;

// When connecting to a test run, the test package (package:test_core)
// spawns each test suite in a separate isolate with a debug name
// prefixed with 'test_suite:'. DevTools should connect to this isolate
// rather than the test runner isolate ('main'), since the test suite
// isolate is where user code actually runs.
// See: https://github.com/flutter/devtools/issues/9747
final testSuiteRef = _isolateStates.keys.firstWhereOrNull(
(IsolateRef ref) => ref.name?.startsWith('test_suite:') ?? false,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this isolate not marked as a system isolate? Ideally it just wouldn't appear in the isolates list.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is the actual test, so it's the thing we do want to find. I don't know if we can make the root isolate (test runner) be not marked as a system isolate. But that is the one we would want to ignore if any probably.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Right, that makes sense. If we mark the main isolate as a system isolate, I'm not sure we'll need this change. We already do it for package:test via dart test, so we should probably make a similar change in flutter_tester to set the main isolate as a system isolate.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If we can achieve the same result that way it would definitely be preferable to matching based on debug name and other heuristics

);
if (testSuiteRef != null) return testSuiteRef;
}

return ref ?? _isolateStates.keys.first;
}

Future<IsolateRef?> _findTestSuiteByRootLibrary(VmService? service) async {
String? projectRootUri;
if (service != null) {
try {
final vm = await service.getVM();
if (service != _service) return null;
// `rootUri` is not a typed field on `VM` in the current vm_service
// version, so read it from the raw JSON response.
projectRootUri = vm.json?['rootUri'] as String?;
} catch (_) {
// If getVM() fails, proceed without project-root scoping.
}
}

for (final isolateState in _isolateStates.values) {
final isolate = await isolateState.isolate;
if (service != _service) return null;

final rootLibraryUri = isolate?.rootLib?.uri;
if (rootLibraryUri == null) continue;

if (_isDartTestRunnerRootLibrary(rootLibraryUri)) continue;

if (_isLikelyUserTestRootLibrary(
rootLibraryUri,
projectRootUri: projectRootUri,
)) {
return isolateState.isolateRef;
}
}

return null;
}

/// Returns true if [uri] looks like the root library of a dart:test runner
/// isolate (as opposed to the user's test suite isolate).
///
/// Note: 'dart_test.kernel', 'package:test_core/', and 'package:test_api/'
/// are implementation details of package:test that DevTools depends on.
/// Changing these in package:test will break this logic.
bool _isDartTestRunnerRootLibrary(String uri) {
return uri.contains('dart_test.kernel') ||
uri.startsWith('package:test_core/') ||
uri.startsWith('package:test_api/');
}

/// Returns true if [uri] looks like the root library of a user test isolate.
///
/// When [projectRootUri] is provided, `file://` URIs must be under it to
/// avoid false positives from other packages in the workspace that happen to
/// contain '/test/' or end with '_test.dart'.
bool _isLikelyUserTestRootLibrary(String uri, {String? projectRootUri}) {
if (uri.startsWith('dart:')) return false;

// Scope file:// URIs to the current project to avoid matching test files
// from other packages in a workspace.
if (projectRootUri != null &&
uri.startsWith('file:') &&
!uri.startsWith(projectRootUri)) {
return false;
}

return uri.endsWith('_test.dart') ||
uri.contains('/test/') ||
uri.contains('\\test\\');
}

void _setSelectedIsolate(IsolateRef? ref) {
_selectedIsolate.value = ref;
}
Expand Down
Loading