| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:fake_async/fake_async.dart'; |
| import 'package:file/memory.dart'; |
| import 'package:file_testing/file_testing.dart'; |
| import 'package:flutter_tools/src/base/file_system.dart'; |
| import 'package:flutter_tools/src/base/logger.dart'; |
| import 'package:flutter_tools/src/convert.dart'; |
| import 'package:flutter_tools/src/tracing.dart'; |
| import 'package:flutter_tools/src/vmservice.dart'; |
| import 'package:vm_service/vm_service.dart' as vm_service; |
| |
| import '../src/common.dart'; |
| import '../src/fake_vm_services.dart'; |
| |
| final vm_service.Isolate fakeUnpausedIsolate = vm_service.Isolate( |
| id: '1', |
| pauseEvent: vm_service.Event( |
| kind: vm_service.EventKind.kResume, |
| timestamp: 0 |
| ), |
| breakpoints: <vm_service.Breakpoint>[], |
| libraries: <vm_service.LibraryRef>[ |
| vm_service.LibraryRef( |
| id: '1', |
| uri: 'file:///hello_world/main.dart', |
| name: '', |
| ), |
| ], |
| livePorts: 0, |
| name: 'test', |
| number: '1', |
| pauseOnExit: false, |
| runnable: true, |
| startTime: 0, |
| isSystemIsolate: false, |
| isolateFlags: <vm_service.IsolateFlag>[], |
| ); |
| |
| final FlutterView fakeFlutterView = FlutterView( |
| id: 'a', |
| uiIsolate: fakeUnpausedIsolate, |
| ); |
| |
| final FakeVmServiceRequest listViews = FakeVmServiceRequest( |
| method: kListViewsMethod, |
| jsonResponse: <String, Object>{ |
| 'views': <Object>[ |
| fakeFlutterView.toJson(), |
| ], |
| }, |
| ); |
| |
| final List<FakeVmServiceRequest> vmServiceSetup = <FakeVmServiceRequest>[ |
| const FakeVmServiceRequest( |
| method: 'streamListen', |
| args: <String, Object>{ |
| 'streamId': vm_service.EventKind.kExtension, |
| } |
| ), |
| listViews, |
| // Satisfies didAwaitFirstFrame |
| const FakeVmServiceRequest( |
| method: 'ext.flutter.didSendFirstFrameRasterizedEvent', |
| args: <String, Object>{ |
| 'isolateId': '1', |
| }, |
| jsonResponse: <String, Object>{ |
| 'enabled': 'true', |
| }, |
| ), |
| ]; |
| |
| void main() { |
| testWithoutContext('Can trace application startup', () async { |
| final BufferLogger logger = BufferLogger.test(); |
| final FileSystem fileSystem = MemoryFileSystem.test(); |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[ |
| ...vmServiceSetup, |
| FakeVmServiceRequest( |
| method: 'getVMTimeline', |
| jsonResponse: vm_service.Timeline( |
| timeExtentMicros: 4, |
| timeOriginMicros: 0, |
| traceEvents: <vm_service.TimelineEvent>[ |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFlutterEngineMainEnterEventName, |
| 'ts': 0, |
| })!, |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFrameworkInitEventName, |
| 'ts': 1, |
| })!, |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFirstFrameBuiltEventName, |
| 'ts': 2, |
| })!, |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFirstFrameRasterizedEventName, |
| 'ts': 3, |
| })!, |
| ], |
| ).toJson(), |
| ), |
| const FakeVmServiceRequest( |
| method: 'setVMTimelineFlags', |
| args: <String, Object>{ |
| 'recordedStreams': <Object>[], |
| }, |
| ), |
| ]); |
| |
| // Validate that old tracing data is deleted. |
| final File outFile = fileSystem.currentDirectory.childFile('start_up_info.json') |
| ..writeAsStringSync('stale'); |
| |
| await downloadStartupTrace(fakeVmServiceHost.vmService, |
| output: fileSystem.currentDirectory, |
| logger: logger, |
| ); |
| |
| expect(outFile, exists); |
| expect(json.decode(outFile.readAsStringSync()), <String, Object>{ |
| 'engineEnterTimestampMicros': 0, |
| 'timeToFrameworkInitMicros': 1, |
| 'timeToFirstFrameRasterizedMicros': 3, |
| 'timeToFirstFrameMicros': 2, |
| 'timeAfterFrameworkInitMicros': 1, |
| }); |
| }); |
| |
| testWithoutContext('throws tool exit if the vmservice disconnects', () async { |
| final BufferLogger logger = BufferLogger.test(); |
| final FileSystem fileSystem = MemoryFileSystem.test(); |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[ |
| ...vmServiceSetup, |
| const FakeVmServiceRequest( |
| method: 'getVMTimeline', |
| errorCode: RPCErrorCodes.kServiceDisappeared, |
| ), |
| const FakeVmServiceRequest( |
| method: 'setVMTimelineFlags', |
| args: <String, Object>{ |
| 'recordedStreams': <Object>[], |
| }, |
| ), |
| ]); |
| |
| await expectLater(() async => downloadStartupTrace(fakeVmServiceHost.vmService, |
| output: fileSystem.currentDirectory, |
| logger: logger, |
| ), throwsToolExit(message: 'The device disconnected before the timeline could be retrieved.')); |
| }); |
| |
| testWithoutContext('throws tool exit if timeline is missing the engine start event', () async { |
| final BufferLogger logger = BufferLogger.test(); |
| final FileSystem fileSystem = MemoryFileSystem.test(); |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[ |
| ...vmServiceSetup, |
| FakeVmServiceRequest( |
| method: 'getVMTimeline', |
| jsonResponse: vm_service.Timeline( |
| timeExtentMicros: 4, |
| timeOriginMicros: 0, |
| traceEvents: <vm_service.TimelineEvent>[], |
| ).toJson(), |
| ), |
| const FakeVmServiceRequest( |
| method: 'setVMTimelineFlags', |
| args: <String, Object>{ |
| 'recordedStreams': <Object>[], |
| }, |
| ), |
| ]); |
| |
| await expectLater(() async => downloadStartupTrace(fakeVmServiceHost.vmService, |
| output: fileSystem.currentDirectory, |
| logger: logger, |
| ), throwsToolExit(message: 'Engine start event is missing in the timeline')); |
| }); |
| |
| testWithoutContext('prints when first frame is taking a long time', () async { |
| final BufferLogger logger = BufferLogger.test(); |
| final FileSystem fileSystem = MemoryFileSystem.test(); |
| final Completer<void> completer = Completer<void>(); |
| await FakeAsync().run((FakeAsync time) { |
| final Map<String, String> extensionData = <String, String>{ |
| 'test': 'data', |
| 'renderedErrorText': 'error text', |
| }; |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[ |
| const FakeVmServiceRequest( |
| method: 'streamListen', |
| args: <String, Object>{ |
| 'streamId': vm_service.EventKind.kExtension, |
| } |
| ), |
| const FakeVmServiceRequest( |
| method: kListViewsMethod, |
| jsonResponse: <String, Object>{ |
| 'views': <Object>[ |
| <String, Object?>{ |
| 'id': '1', |
| // No isolate, no views. |
| 'isolate': null, |
| } |
| ], |
| }, |
| ), |
| FakeVmServiceStreamResponse( |
| streamId: 'Extension', |
| event: vm_service.Event( |
| timestamp: 0, |
| extensionKind: 'Flutter.Error', |
| extensionData: vm_service.ExtensionData.parse(extensionData), |
| kind: vm_service.EventStreams.kExtension, |
| ), |
| ), |
| ]); |
| unawaited(downloadStartupTrace(fakeVmServiceHost.vmService, |
| output: fileSystem.currentDirectory, |
| logger: logger, |
| )); |
| time.elapse(const Duration(seconds: 11)); |
| time.flushMicrotasks(); |
| completer.complete(); |
| return completer.future; |
| }); |
| expect(logger.statusText, contains('First frame is taking longer than expected')); |
| expect(logger.traceText, contains('View ID: 1')); |
| expect(logger.traceText, contains('No isolate ID associated with the view')); |
| expect(logger.traceText, contains('Flutter.Error: [ExtensionData {test: data, renderedErrorText: error text}]')); |
| }); |
| |
| testWithoutContext('throws tool exit if first frame events are missing', () async { |
| final BufferLogger logger = BufferLogger.test(); |
| final FileSystem fileSystem = MemoryFileSystem.test(); |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[ |
| ...vmServiceSetup, |
| FakeVmServiceRequest( |
| method: 'getVMTimeline', |
| jsonResponse: vm_service.Timeline( |
| timeExtentMicros: 4, |
| timeOriginMicros: 0, |
| traceEvents: <vm_service.TimelineEvent>[ |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFlutterEngineMainEnterEventName, |
| 'ts': 0, |
| })!, |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFrameworkInitEventName, |
| 'ts': 1, |
| })!, |
| ], |
| ).toJson(), |
| ), |
| const FakeVmServiceRequest( |
| method: 'setVMTimelineFlags', |
| args: <String, Object>{ |
| 'recordedStreams': <Object>[], |
| }, |
| ), |
| ]); |
| |
| await expectLater(() async => downloadStartupTrace(fakeVmServiceHost.vmService, |
| output: fileSystem.currentDirectory, |
| logger: logger, |
| ), throwsToolExit(message: 'First frame events are missing in the timeline')); |
| }); |
| |
| testWithoutContext('Can trace application startup without awaiting for first frame', () async { |
| final BufferLogger logger = BufferLogger.test(); |
| final FileSystem fileSystem = MemoryFileSystem.test(); |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[ |
| FakeVmServiceRequest( |
| method: 'getVMTimeline', |
| jsonResponse: vm_service.Timeline( |
| timeExtentMicros: 4, |
| timeOriginMicros: 0, |
| traceEvents: <vm_service.TimelineEvent>[ |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFlutterEngineMainEnterEventName, |
| 'ts': 0, |
| })!, |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFrameworkInitEventName, |
| 'ts': 1, |
| })!, |
| ], |
| ).toJson(), |
| ), |
| const FakeVmServiceRequest( |
| method: 'setVMTimelineFlags', |
| args: <String, Object>{ |
| 'recordedStreams': <Object>[], |
| }, |
| ), |
| ]); |
| |
| final File outFile = fileSystem.currentDirectory.childFile('start_up_info.json'); |
| |
| await downloadStartupTrace(fakeVmServiceHost.vmService, |
| output: fileSystem.currentDirectory, |
| logger: logger, |
| awaitFirstFrame: false, |
| ); |
| |
| expect(outFile, exists); |
| expect(json.decode(outFile.readAsStringSync()), <String, Object>{ |
| 'engineEnterTimestampMicros': 0, |
| 'timeToFrameworkInitMicros': 1, |
| }); |
| }); |
| |
| testWithoutContext('downloadStartupTrace also downloads the timeline', () async { |
| final BufferLogger logger = BufferLogger.test(); |
| final FileSystem fileSystem = MemoryFileSystem.test(); |
| final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <FakeVmServiceRequest>[ |
| ...vmServiceSetup, |
| FakeVmServiceRequest( |
| method: 'getVMTimeline', |
| jsonResponse: vm_service.Timeline( |
| timeExtentMicros: 4, |
| timeOriginMicros: 0, |
| traceEvents: <vm_service.TimelineEvent>[ |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFlutterEngineMainEnterEventName, |
| 'ts': 0, |
| })!, |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFrameworkInitEventName, |
| 'ts': 1, |
| })!, |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFirstFrameBuiltEventName, |
| 'ts': 2, |
| })!, |
| vm_service.TimelineEvent.parse(<String, Object>{ |
| 'name': kFirstFrameRasterizedEventName, |
| 'ts': 3, |
| })!, |
| ], |
| ).toJson(), |
| ), |
| const FakeVmServiceRequest( |
| method: 'setVMTimelineFlags', |
| args: <String, Object>{ |
| 'recordedStreams': <Object>[], |
| }, |
| ), |
| ]); |
| |
| // Validate that old tracing data is deleted. |
| final File timelineFile = fileSystem.currentDirectory.childFile('start_up_timeline.json') |
| ..writeAsStringSync('stale'); |
| |
| await downloadStartupTrace(fakeVmServiceHost.vmService, |
| output: fileSystem.currentDirectory, |
| logger: logger, |
| ); |
| |
| final Map<String, Object> expectedTimeline = <String, Object>{ |
| 'type': 'Timeline', |
| 'traceEvents': <Object>[ |
| <String, Object>{ |
| 'name': 'FlutterEngineMainEnter', |
| 'ts': 0, |
| 'type': 'TimelineEvent', |
| }, |
| <String, Object>{ |
| 'name': 'Framework initialization', |
| 'ts': 1, |
| 'type': 'TimelineEvent', |
| }, |
| <String, Object>{ |
| 'name': 'Widgets built first useful frame', |
| 'ts': 2, |
| 'type': 'TimelineEvent', |
| }, |
| <String, Object>{ |
| 'name': 'Rasterized first useful frame', |
| 'ts': 3, |
| 'type': 'TimelineEvent', |
| }, |
| ], |
| 'timeOriginMicros': 0, |
| 'timeExtentMicros': 4, |
| }; |
| |
| expect(timelineFile, exists); |
| expect(json.decode(timelineFile.readAsStringSync()), expectedTimeline); |
| }); |
| } |