[flutter_tools] Call reassemble with DWDS 24.3.7 and update hot reload and restart analytics (#165006)

https://github.com/dart-lang/webdev/issues/2584

Reassemble was being called in DWDS in the injected client until
v24.3.7. Flutter tools should now instead be the one to call the service
extension. Now that it's in Flutter tools, we can also report how long
it took. Similarly, we should update analytics on various things like,
whether there was a reload rejection, how long the compile took, and
more.

Adds test to check that these analytics are being reported correctly.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
This commit is contained in:
Srujan Gaddam 2025-03-12 08:16:49 -07:00 committed by GitHub
parent eef31a1c2b
commit 0bdb4d68b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 280 additions and 58 deletions

View File

@ -927,7 +927,7 @@ class AppDomain extends Domain {
final Map<String, Object?>? result = await device.vmService!.invokeFlutterExtensionRpcRaw(
methodName,
args: params,
isolateId: views.first.uiIsolate!.id!,
isolateId: views.first.uiIsolate!.id,
);
if (result == null) {
throw DaemonException('method not available: $methodName');

View File

@ -380,13 +380,15 @@ class UpdateFSReport {
Duration compileDuration = Duration.zero,
Duration transferDuration = Duration.zero,
Duration findInvalidatedDuration = Duration.zero,
bool hotReloadRejected = false,
}) : _success = success,
_invalidatedSourcesCount = invalidatedSourcesCount,
_syncedBytes = syncedBytes,
_scannedSourcesCount = scannedSourcesCount,
_compileDuration = compileDuration,
_transferDuration = transferDuration,
_findInvalidatedDuration = findInvalidatedDuration;
_findInvalidatedDuration = findInvalidatedDuration,
_hotReloadRejected = hotReloadRejected;
bool get success => _success;
int get invalidatedSourcesCount => _invalidatedSourcesCount;
@ -396,6 +398,12 @@ class UpdateFSReport {
Duration get transferDuration => _transferDuration;
Duration get findInvalidatedDuration => _findInvalidatedDuration;
/// Whether there was a hot reload rejection in this compile.
///
/// On the web, hot reload can be rejected during compile time instead of at
/// runtime.
bool get hotReloadRejected => _hotReloadRejected;
bool _success;
int _invalidatedSourcesCount;
int _syncedBytes;
@ -403,6 +411,7 @@ class UpdateFSReport {
Duration _compileDuration;
Duration _transferDuration;
Duration _findInvalidatedDuration;
bool _hotReloadRejected;
void incorporateResults(UpdateFSReport report) {
if (!report._success) {
@ -414,6 +423,9 @@ class UpdateFSReport {
_compileDuration += report._compileDuration;
_transferDuration += report._transferDuration;
_findInvalidatedDuration += report._findInvalidatedDuration;
if (report._hotReloadRejected) {
_hotReloadRejected = true;
}
}
}

View File

@ -1139,7 +1139,14 @@ class WebDevFS implements DevFS {
recompileRestart: fullRestart,
);
if (compilerOutput == null || compilerOutput.errorCount > 0) {
return UpdateFSReport();
return UpdateFSReport(
// TODO(srujzs): We're currently reliant on compile error string parsing
// as hot reload rejections are sent to stderr just like other
// compilation errors. Ideally, we should have some shared parsing
// functionality, but that would require a shared package.
// See https://github.com/dart-lang/sdk/issues/60275.
hotReloadRejected: compilerOutput?.errorMessage?.contains('Hot reload rejected') ?? false,
);
}
// Only update the last compiled time if we successfully compiled.

View File

@ -432,19 +432,40 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
status = _logger.startProgress('Performing hot reload...', progressId: 'hot.reload');
}
final String targetPlatform = getNameForTargetPlatform(TargetPlatform.web_javascript);
final String sdkName = await device!.device!.sdkNameAndVersion;
late UpdateFSReport report;
if (debuggingOptions.buildInfo.isDebug && !debuggingOptions.webUseWasm) {
await runSourceGenerators();
// Don't reset the resident compiler for web, since the extra recompile is
// wasteful.
final UpdateFSReport report = await _updateDevFS(
fullRestart: fullRestart,
resetCompiler: false,
);
report = await _updateDevFS(fullRestart: fullRestart, resetCompiler: false);
if (report.success) {
device!.generator!.accept();
} else {
status.stop();
await device!.generator!.reject();
if (report.hotReloadRejected) {
// We cannot capture the reason why the reload was rejected as it may
// contain user information.
HotEvent(
'reload-reject',
targetPlatform: targetPlatform,
sdkName: sdkName,
emulator: false,
fullRestart: fullRestart,
).send();
_analytics.send(
Event.hotRunnerInfo(
label: 'reload-reject',
targetPlatform: targetPlatform,
sdkName: sdkName,
emulator: false,
fullRestart: fullRestart,
),
);
}
return OperationResult(1, 'Failed to recompile application.');
}
} else {
@ -469,6 +490,8 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
}
}
late Duration reloadDuration;
late Duration reassembleDuration;
try {
if (!deviceIsDebuggable) {
_logger.printStatus('Recompile complete. Page requires refresh.');
@ -482,7 +505,9 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
} else {
// Isolates don't work on web. For lack of a better value, pass an
// empty string for the isolate id.
final DateTime reloadStart = _systemClock.now();
final vmservice.ReloadReport report = await _vmService.service.reloadSources('');
reloadDuration = _systemClock.now().difference(reloadStart);
final ReloadReportContents contents = ReloadReportContents.fromReloadReport(report);
final bool success = contents.success ?? false;
if (!success) {
@ -498,6 +523,21 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
}
return OperationResult(1, reloadFailedMessage);
}
String? failedReassemble;
final DateTime reassembleStart = _systemClock.now();
await _vmService
.flutterReassemble(isolateId: null)
.then(
(Object? o) => o,
onError: (Object error, StackTrace stackTrace) {
failedReassemble = 'Reassembling failed: $error\n$stackTrace';
_logger.printError(failedReassemble!);
},
);
reassembleDuration = _systemClock.now().difference(reassembleStart);
if (failedReassemble != null) {
return OperationResult(1, failedReassemble!);
}
}
} else {
// On non-debug builds, a hard refresh is required to ensure the
@ -522,11 +562,6 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
// Don't track restart times for dart2js builds or web-server devices.
if (debuggingOptions.buildInfo.isDebug && deviceIsDebuggable) {
// TODO(srujzs): There are a number of fields that the VM tracks in the
// analytics that we do not for both hot restart and reload. We should
// unify that.
final String targetPlatform = getNameForTargetPlatform(TargetPlatform.web_javascript);
final String sdkName = await device!.device!.sdkNameAndVersion;
if (fullRestart) {
_analytics.send(
Event.timing(
@ -543,6 +578,12 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
fullRestart: true,
reason: reason,
overallTimeInMs: elapsed.inMilliseconds,
syncedBytes: report.syncedBytes,
invalidatedSourcesCount: report.invalidatedSourcesCount,
transferTimeInMs: report.transferDuration.inMilliseconds,
compileTimeInMs: report.compileDuration.inMilliseconds,
findInvalidatedTimeInMs: report.findInvalidatedDuration.inMilliseconds,
scannedSourcesCount: report.scannedSourcesCount,
).send();
_analytics.send(
Event.hotRunnerInfo(
@ -553,6 +594,12 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
fullRestart: true,
reason: reason,
overallTimeInMs: elapsed.inMilliseconds,
syncedBytes: report.syncedBytes,
invalidatedSourcesCount: report.invalidatedSourcesCount,
transferTimeInMs: report.transferDuration.inMilliseconds,
compileTimeInMs: report.compileDuration.inMilliseconds,
findInvalidatedTimeInMs: report.findInvalidatedDuration.inMilliseconds,
scannedSourcesCount: report.scannedSourcesCount,
),
);
} else {
@ -571,6 +618,14 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
fullRestart: false,
reason: reason,
overallTimeInMs: elapsed.inMilliseconds,
syncedBytes: report.syncedBytes,
invalidatedSourcesCount: report.invalidatedSourcesCount,
transferTimeInMs: report.transferDuration.inMilliseconds,
compileTimeInMs: report.compileDuration.inMilliseconds,
findInvalidatedTimeInMs: report.findInvalidatedDuration.inMilliseconds,
scannedSourcesCount: report.scannedSourcesCount,
reassembleTimeInMs: reassembleDuration.inMilliseconds,
reloadVMTimeInMs: reloadDuration.inMilliseconds,
).send();
_analytics.send(
Event.hotRunnerInfo(
@ -581,6 +636,14 @@ Please provide a valid TCP port (an integer between 0 and 65535, inclusive).
fullRestart: false,
reason: reason,
overallTimeInMs: elapsed.inMilliseconds,
syncedBytes: report.syncedBytes,
invalidatedSourcesCount: report.invalidatedSourcesCount,
transferTimeInMs: report.transferDuration.inMilliseconds,
compileTimeInMs: report.compileDuration.inMilliseconds,
findInvalidatedTimeInMs: report.findInvalidatedDuration.inMilliseconds,
scannedSourcesCount: report.scannedSourcesCount,
reassembleTimeInMs: reassembleDuration.inMilliseconds,
reloadVMTimeInMs: reloadDuration.inMilliseconds,
),
);
}

View File

@ -298,7 +298,7 @@ class FlutterResidentDevtoolsHandler implements ResidentDevtoolsHandler {
await device.vmService!.invokeFlutterExtensionRpcRaw(
method,
args: params,
isolateId: views.first.uiIsolate!.id!,
isolateId: views.first.uiIsolate!.id,
);
}

View File

@ -1416,7 +1416,7 @@ Future<ReassembleResult> _defaultReassembleHelper(
// If the tool identified a change in a single widget, do a fast instead
// of a full reassemble.
final Future<void> reassembleWork = device.vmService!.flutterReassemble(
isolateId: view.uiIsolate!.id!,
isolateId: view.uiIsolate!.id,
);
reassembleFutures.add(
reassembleWork.then(

View File

@ -684,7 +684,7 @@ class FlutterVmService {
);
}
Future<Map<String, Object?>?> flutterReassemble({required String isolateId}) {
Future<Map<String, Object?>?> flutterReassemble({required String? isolateId}) {
return invokeFlutterExtensionRpcRaw('ext.flutter.reassemble', isolateId: isolateId);
}
@ -803,12 +803,12 @@ class FlutterVmService {
/// available, returns null.
Future<Map<String, Object?>?> invokeFlutterExtensionRpcRaw(
String method, {
required String isolateId,
required String? isolateId,
Map<String, Object?>? args,
}) async {
final vm_service.Response? response = await _checkedCallServiceExtension(
method,
args: <String, Object?>{'isolateId': isolateId, ...?args},
args: <String, Object?>{if (isolateId != null) 'isolateId': isolateId, ...?args},
);
return response?.json;
}

View File

@ -13,7 +13,7 @@ dependencies:
archive: 3.6.1
args: 2.6.0
dds: 5.0.0
dwds: 24.3.6
dwds: 24.3.7
code_builder: 4.10.1
completion: 1.0.1
coverage: 1.11.1
@ -122,4 +122,4 @@ dartdoc:
# Exclude this package from the hosted API docs.
nodoc: true
# PUBSPEC CHECKSUM: 5a6c
# PUBSPEC CHECKSUM: 776d

View File

@ -260,6 +260,11 @@ class FakeWebDevice extends Fake implements Device {
return true;
}
@override
Future<String> get sdkNameAndVersion async {
return 'Flutter Tools';
}
@override
Future<LaunchResult> startApp(
ApplicationPackage? package, {

View File

@ -75,6 +75,11 @@ const List<VmServiceExpectation> kAttachExpectations = <VmServiceExpectation>[
...kAttachIsolateExpectations,
];
const List<String> kDdcLibraryBundleFlags = <String>[
'--dartdevc-module-format=ddc',
'--dartdevc-canary',
];
void main() {
late FakeDebugConnection debugConnection;
late FakeChromeDevice chromeDevice;
@ -725,13 +730,29 @@ name: my_app
flutterDevice,
logger: logger,
systemClock: SystemClock.fixed(DateTime(2001)),
debuggingOptions: DebuggingOptions.enabled(
const BuildInfo(
BuildMode.debug,
null,
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
// Hot reload only supported with these flags for now.
extraFrontEndOptions: kDdcLibraryBundleFlags,
),
),
);
fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
...kAttachExpectations,
const FakeVmServiceRequest(
method: kHotRestartServiceName,
jsonResponse: <String, Object>{'type': 'Success'},
method: kReloadSourcesServiceName,
args: <String, Object>{'isolateId': ''},
jsonResponse: <String, Object>{'type': 'ReloadReport', 'success': true},
),
const FakeVmServiceRequest(
method: 'ext.flutter.reassemble',
jsonResponse: <String, Object>{'type': 'ReloadReport', 'success': true},
),
const FakeVmServiceRequest(
method: 'streamListen',
@ -769,7 +790,7 @@ name: my_app
final OperationResult result = await residentWebRunner.restart();
expect(logger.statusText, contains('Restarted application in'));
expect(logger.statusText, contains('Reloaded application in'));
expect(result.code, 0);
expect(webDevFS.mainUri.toString(), contains('entrypoint.dart'));
@ -777,24 +798,26 @@ name: my_app
fakeAnalytics.sentEvents,
contains(
Event.hotRunnerInfo(
label: 'restart',
label: 'reload',
targetPlatform: 'web-javascript',
sdkName: '',
emulator: false,
fullRestart: true,
fullRestart: false,
overallTimeInMs: 0,
syncedBytes: 0,
invalidatedSourcesCount: 0,
transferTimeInMs: 0,
compileTimeInMs: 0,
findInvalidatedTimeInMs: 0,
scannedSourcesCount: 0,
reassembleTimeInMs: 0,
reloadVMTimeInMs: 0,
),
),
);
expect(
fakeAnalytics.sentEvents,
contains(
Event.timing(
workflow: 'hot',
variableName: 'web-incremental-restart',
elapsedMilliseconds: 0,
),
),
contains(Event.timing(workflow: 'hot', variableName: 'reload', elapsedMilliseconds: 0)),
);
},
overrides: <Type, Generator>{
@ -807,20 +830,31 @@ name: my_app
);
testUsingContext(
'Can hot restart after attaching',
'Hot reload reject reports correct analytics',
() async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(
flutterDevice,
logger: logger,
systemClock: SystemClock.fixed(DateTime(2001)),
debuggingOptions: DebuggingOptions.enabled(
const BuildInfo(
BuildMode.debug,
null,
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
// Hot reload only supported with these flags for now.
extraFrontEndOptions: kDdcLibraryBundleFlags,
),
),
);
fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
...kAttachExpectations,
const FakeVmServiceRequest(
method: kHotRestartServiceName,
jsonResponse: <String, Object>{'type': 'Success'},
method: 'streamListen',
args: <String, Object>{'streamId': 'Isolate'},
),
],
);
@ -848,39 +882,25 @@ name: my_app
final Completer<DebugConnectionInfo> connectionInfoCompleter =
Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
await connectionInfoCompleter.future;
final OperationResult result = await residentWebRunner.restart(fullRestart: true);
final DebugConnectionInfo debugConnectionInfo = await connectionInfoCompleter.future;
// Ensure that generated entrypoint is generated correctly.
expect(webDevFS.mainUri, isNotNull);
final String entrypointContents = fileSystem.file(webDevFS.mainUri).readAsStringSync();
expect(entrypointContents, contains('// Flutter web bootstrap script'));
expect(entrypointContents, contains("import 'dart:ui_web' as ui_web;"));
expect(entrypointContents, contains('await ui_web.bootstrapEngine('));
expect(debugConnectionInfo, isNotNull);
expect(logger.statusText, contains('Restarted application in'));
expect(result.code, 0);
webDevFS.report = UpdateFSReport(hotReloadRejected: true);
final OperationResult result = await residentWebRunner.restart();
expect(result.code, 1);
expect(webDevFS.mainUri.toString(), contains('entrypoint.dart'));
expect(
fakeAnalytics.sentEvents,
contains(
Event.hotRunnerInfo(
label: 'restart',
label: 'reload-reject',
targetPlatform: 'web-javascript',
sdkName: '',
emulator: false,
fullRestart: true,
overallTimeInMs: 0,
),
),
);
expect(
fakeAnalytics.sentEvents,
contains(
Event.timing(
workflow: 'hot',
variableName: 'web-incremental-restart',
elapsedMilliseconds: 0,
fullRestart: false,
),
),
);
@ -894,6 +914,120 @@ name: my_app
},
);
// Hot restart is available with and without the DDC library bundle format.
// Test one extra config where `fullRestart` is false without the DDC library
// bundle format - we should do a hot restart in this case because hot reload
// is not available.
for (final (List<String> flags, bool fullRestart) in <(List<String>, bool)>[
(kDdcLibraryBundleFlags, true),
(<String>[], true),
(<String>[], false),
]) {
testUsingContext(
'Can hot restart after attaching with flags: $flags fullRestart: $fullRestart',
() async {
final BufferLogger logger = BufferLogger.test();
final ResidentRunner residentWebRunner = setUpResidentRunner(
flutterDevice,
logger: logger,
systemClock: SystemClock.fixed(DateTime(2001)),
debuggingOptions: DebuggingOptions.enabled(
BuildInfo(
BuildMode.debug,
null,
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
extraFrontEndOptions: flags,
),
),
);
fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[
...kAttachExpectations,
const FakeVmServiceRequest(
method: kHotRestartServiceName,
jsonResponse: <String, Object>{'type': 'Success'},
),
],
);
setupMocks();
final TestChromiumLauncher chromiumLauncher = TestChromiumLauncher();
final FakeProcess process = FakeProcess();
final Chromium chrome = Chromium(
1,
chromeConnection,
chromiumLauncher: chromiumLauncher,
process: process,
logger: logger,
);
chromiumLauncher.setInstance(chrome);
flutterDevice.device = GoogleChromeDevice(
fileSystem: fileSystem,
chromiumLauncher: chromiumLauncher,
logger: BufferLogger.test(),
platform: FakePlatform(),
processManager: FakeProcessManager.any(),
);
webDevFS.report = UpdateFSReport(success: true);
final Completer<DebugConnectionInfo> connectionInfoCompleter =
Completer<DebugConnectionInfo>();
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
await connectionInfoCompleter.future;
final OperationResult result = await residentWebRunner.restart(fullRestart: fullRestart);
// Ensure that generated entrypoint is generated correctly.
expect(webDevFS.mainUri, isNotNull);
final String entrypointContents = fileSystem.file(webDevFS.mainUri).readAsStringSync();
expect(entrypointContents, contains('// Flutter web bootstrap script'));
expect(entrypointContents, contains("import 'dart:ui_web' as ui_web;"));
expect(entrypointContents, contains('await ui_web.bootstrapEngine('));
expect(logger.statusText, contains('Restarted application in'));
expect(result.code, 0);
expect(
fakeAnalytics.sentEvents,
contains(
Event.hotRunnerInfo(
label: 'restart',
targetPlatform: 'web-javascript',
sdkName: '',
emulator: false,
fullRestart: true,
overallTimeInMs: 0,
syncedBytes: 0,
invalidatedSourcesCount: 0,
transferTimeInMs: 0,
compileTimeInMs: 0,
findInvalidatedTimeInMs: 0,
scannedSourcesCount: 0,
),
),
);
expect(
fakeAnalytics.sentEvents,
contains(
Event.timing(
workflow: 'hot',
variableName: 'web-incremental-restart',
elapsedMilliseconds: 0,
),
),
);
},
overrides: <Type, Generator>{
Analytics: () => fakeAnalytics,
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
FeatureFlags: enableExplicitPackageDependencies,
Pub: FakePubWithPrimedDeps.new,
},
);
}
testUsingContext(
'Can hot restart after attaching with web-server device',
() async {
@ -1130,7 +1264,8 @@ name: my_app
trackWidgetCreation: true,
treeShakeIcons: false,
packageConfigPath: '.dart_tool/package_config.json',
extraFrontEndOptions: <String>['--dartdevc-module-format=ddc', '--dartdevc-canary'],
// Hot reload only supported with these flags for now.
extraFrontEndOptions: kDdcLibraryBundleFlags,
),
),
);