| // Copyright 2019 The Chromium 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:collection/collection.dart' show IterableExtension; |
| import 'package:devtools_app/devtools_app.dart'; |
| import 'package:devtools_shared/devtools_shared.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:vm_service/vm_service.dart'; |
| |
| import 'generated.mocks.dart'; |
| import 'mocks.dart'; |
| |
| class FakeVmServiceWrapper extends Fake implements VmServiceWrapper { |
| FakeVmServiceWrapper( |
| this._vmFlagManager, |
| this._timelineData, |
| this._socketProfile, |
| this._httpProfile, |
| this._memoryData, |
| this._allocationData, |
| CpuSamples? cpuSamples, |
| CpuSamples? allocationSamples, |
| this._resolvedUriMap, |
| this._classList, |
| List<({String flagName, String value})>? flags, |
| ) : _startingSockets = _socketProfile?.sockets ?? [], |
| _startingRequests = _httpProfile?.requests ?? [], |
| cpuSamples = cpuSamples ?? _defaultProfile, |
| allocationSamples = allocationSamples ?? _defaultProfile { |
| _reverseResolvedUriMap = <String, String>{}; |
| if (_resolvedUriMap != null) { |
| for (final e in _resolvedUriMap.entries) { |
| _reverseResolvedUriMap![e.value] = e.key; |
| } |
| } |
| |
| if (flags != null) { |
| for (final flag in flags) { |
| unawaited(setFlag(flag.flagName, flag.value)); |
| } |
| } |
| } |
| |
| static final _defaultProfile = CpuSamples.parse({ |
| 'samplePeriod': 50, |
| 'maxStackDepth': 12, |
| 'sampleCount': 0, |
| 'timeOriginMicros': 47377796685, |
| 'timeExtentMicros': 3000, |
| 'pid': 54321, |
| 'functions': <Object?>[], |
| 'samples': <Object?>[], |
| })!; |
| |
| CpuSamples cpuSamples; |
| |
| CpuSamples? allocationSamples; |
| |
| /// Specifies the return value of `httpEnableTimelineLogging`. |
| bool httpEnableTimelineLoggingResult = true; |
| |
| /// Specifies the return value of isHttpProfilingAvailable. |
| bool isHttpProfilingAvailableResult = false; |
| |
| /// Specifies the return value of `socketProfilingEnabled`. |
| bool socketProfilingEnabledResult = true; |
| |
| /// Specifies the dart:io service extension version. |
| SemanticVersion dartIoVersion = SemanticVersion(major: 1, minor: 3); |
| |
| final VmFlagManager _vmFlagManager; |
| final PerfettoTimeline? _timelineData; |
| SocketProfile? _socketProfile; |
| final List<SocketStatistic> _startingSockets; |
| HttpProfile? _httpProfile; |
| final List<HttpProfileRequest> _startingRequests; |
| final SamplesMemoryJson? _memoryData; |
| final AllocationMemoryJson? _allocationData; |
| final Map<String, String>? _resolvedUriMap; |
| final ClassList? _classList; |
| late final Map<String, String>? _reverseResolvedUriMap; |
| final _gcEventStream = StreamController<Event>.broadcast(); |
| |
| final _flags = <String, List<Flag?>>{ |
| 'flags': <Flag>[ |
| Flag( |
| name: 'flag 1 name', |
| comment: 'flag 1 comment contains some very long text ' |
| 'that the renderer will have to wrap around to prevent ' |
| 'it from overflowing the screen. This will cause a ' |
| 'failure if one of the two Row entries the flags lay out ' |
| 'in is not wrapped in an Expanded(), which tells the Row ' |
| 'allocate only the remaining space to the Expanded. ' |
| 'Without the expanded, the underlying RichTexts will try ' |
| 'to consume as much of the layout as they can and cause ' |
| 'an overflow.', |
| valueAsString: 'flag 1 value', |
| modified: false, |
| ), |
| Flag( |
| name: profiler, |
| comment: 'Mock Flag', |
| valueAsString: 'true', |
| modified: false, |
| ), |
| Flag( |
| name: profilePeriod, |
| comment: 'Mock Flag', |
| valueAsString: CpuSamplingRate.medium.value, |
| modified: false, |
| ), |
| ], |
| }; |
| |
| @override |
| Future<CpuSamples> getCpuSamples( |
| String isolateId, |
| int timeOriginMicros, |
| int timeExtentMicros, |
| ) { |
| return Future.value(cpuSamples); |
| } |
| |
| @override |
| Future<UriList> lookupPackageUris(String isolateId, List<String> uris) { |
| return Future.value( |
| UriList( |
| uris: _resolvedUriMap != null |
| ? (uris.map((e) => _resolvedUriMap[e]).toList()) |
| : null, |
| ), |
| ); |
| } |
| |
| @override |
| Future<UriList> lookupResolvedPackageUris( |
| String isolateId, |
| List<String> uris, { |
| bool? local, |
| }) { |
| return Future.value( |
| UriList( |
| uris: _reverseResolvedUriMap != null |
| ? (uris.map((e) => _reverseResolvedUriMap[e]).toList()) |
| : null, |
| ), |
| ); |
| } |
| |
| @override |
| String get wsUri => 'ws://127.0.0.1:56137/ISsyt6ki0no=/ws'; |
| |
| @override |
| Future<void> forEachIsolate(Future<void> Function(IsolateRef) callback) => |
| callback( |
| IsolateRef.parse( |
| { |
| 'id': 'fake_isolate_id', |
| }, |
| )!, |
| ); |
| |
| @override |
| Future<AllocationProfile> getAllocationProfile( |
| String isolateId, { |
| bool? reset, |
| bool? gc, |
| }) { |
| final memberStats = <ClassHeapStats>[]; |
| for (final data in _allocationData!.data) { |
| final stats = ClassHeapStats( |
| classRef: data.classRef, |
| accumulatedSize: 0, |
| bytesCurrent: data.bytesCurrent, |
| instancesAccumulated: 0, |
| instancesCurrent: data.instancesCurrent, |
| ); |
| stats.json = data.json; |
| memberStats.add(stats); |
| } |
| final allocationProfile = AllocationProfile( |
| members: memberStats, |
| memoryUsage: MemoryUsage( |
| externalUsage: 10000000, |
| heapCapacity: 20000000, |
| heapUsage: 7777777, |
| ), |
| ); |
| |
| allocationProfile.json = allocationProfile.toJson(); |
| // Fake GC statistics |
| allocationProfile.json![AllocationProfilePrivateViewExtension.heapsKey] = |
| <String, dynamic>{ |
| AllocationProfilePrivateViewExtension.newSpaceKey: <String, dynamic>{ |
| GCStats.usedKey: 1234, |
| GCStats.capacityKey: 12345, |
| GCStats.collectionsKey: 42, |
| GCStats.timeKey: 69, |
| }, |
| AllocationProfilePrivateViewExtension.oldSpaceKey: <String, dynamic>{ |
| GCStats.usedKey: 4321, |
| GCStats.capacityKey: 54321, |
| GCStats.collectionsKey: 24, |
| GCStats.timeKey: 96, |
| }, |
| }; |
| |
| return Future.value(allocationProfile); |
| } |
| |
| @override |
| Future<CpuSamples> getAllocationTraces( |
| String isolateId, { |
| int? timeOriginMicros, |
| int? timeExtentMicros, |
| String? classId, |
| }) { |
| return Future.value(allocationSamples!); |
| } |
| |
| @override |
| Future<Success> setTraceClassAllocation( |
| String isolateId, |
| String classId, |
| bool enable, |
| ) => |
| Future.value(Success()); |
| |
| @override |
| Future<HeapSnapshotGraph> getHeapSnapshotGraph( |
| IsolateRef isolateRef, { |
| bool calculateReferrers = false, |
| bool decodeExternalProperties = false, |
| bool decodeIdentityHashCodes = false, |
| bool decodeObjectData = false, |
| }) async { |
| // Simulate a snapshot that takes .5 seconds. |
| await Future.delayed(const Duration(milliseconds: 500)); |
| final result = MockHeapSnapshotGraph(); |
| when(result.name).thenReturn('name'); |
| when(result.classes).thenReturn([]); |
| when(result.objects).thenReturn([]); |
| when(result.externalProperties).thenReturn([]); |
| when(result.externalSize).thenReturn(0); |
| when(result.shallowSize).thenReturn(0); |
| return result; |
| } |
| |
| @override |
| Future<Isolate> getIsolate(String isolateId) { |
| return Future.value( |
| Isolate.parse( |
| { |
| 'rootLib': LibraryRef.parse( |
| { |
| 'name': 'fake_isolate_name', |
| 'uri': 'package:fake_uri_root/main.dart', |
| }, |
| ), |
| 'extensionRPCs': ['ext.dart.io.getHttpProfile'], |
| }, |
| )!, |
| ); |
| } |
| |
| @override |
| Future<Obj> getObject( |
| String isolateId, |
| String objectId, { |
| int? offset, |
| int? count, |
| }) { |
| return Future.value(MockObj()); |
| } |
| |
| @override |
| Future<MemoryUsage> getMemoryUsage(String isolateId) { |
| if (_memoryData == null) { |
| throw StateError('_memoryData was not provided to FakeServiceManager'); |
| } |
| |
| final heapSample = _memoryData.data.first; |
| return Future.value( |
| MemoryUsage( |
| externalUsage: heapSample.external, |
| heapCapacity: heapSample.capacity, |
| heapUsage: heapSample.used, |
| ), |
| ); |
| } |
| |
| @override |
| Future<ScriptList> getScripts(String isolateId) { |
| return Future.value(ScriptList(scripts: [])); |
| } |
| |
| @override |
| Future<Stack> getStack(String isolateId, {int? limit}) { |
| return Future.value(Stack(frames: [], messages: [], truncated: false)); |
| } |
| |
| @override |
| Future<Success> setFlag(String name, String value) { |
| final flags = _flags['flags']!; |
| final existingFlag = flags.firstWhereOrNull((f) => f?.name == name); |
| if (existingFlag != null) { |
| existingFlag.valueAsString = value; |
| } else { |
| flags.add( |
| Flag.parse({ |
| 'name': name, |
| 'comment': 'Mock Flag', |
| 'modified': true, |
| 'valueAsString': value, |
| }), |
| ); |
| } |
| |
| final fakeVmFlagUpdateEvent = Event( |
| kind: EventKind.kVMFlagUpdate, |
| flag: name, |
| newValue: value, |
| timestamp: 1, // 1 is arbitrary. |
| ); |
| // This library is conceptually for testing even though it is in its own |
| // package to support code reuse. |
| // ignore: invalid_use_of_visible_for_testing_member |
| _vmFlagManager.handleVmEvent(fakeVmFlagUpdateEvent); |
| return Future.value(Success()); |
| } |
| |
| @override |
| Future<FlagList> getFlagList() => |
| Future.value(FlagList.parse(_flags) ?? FlagList(flags: [])); |
| |
| final _vmTimelineFlags = <String, Object?>{ |
| 'type': 'TimelineFlags', |
| 'recordedStreams': [], |
| 'availableStreams': [], |
| }; |
| |
| @override |
| Future<FakeVM> getVM() => Future.value(FakeVM()); |
| |
| @override |
| Future<Success> setVMTimelineFlags(List<String> recordedStreams) { |
| _vmTimelineFlags['recordedStreams'] = recordedStreams; |
| return Future.value(Success()); |
| } |
| |
| @override |
| Future<TimelineFlags> getVMTimelineFlags() => |
| Future.value(TimelineFlags.parse(_vmTimelineFlags)!); |
| |
| @override |
| Future<PerfettoTimeline> getPerfettoVMTimeline({ |
| int? timeOriginMicros, |
| int? timeExtentMicros, |
| }) => |
| _getPerfettoVMTimeline(); |
| |
| @override |
| Future<PerfettoTimeline> getPerfettoVMTimelineWithCpuSamplesWrapper({ |
| int? timeOriginMicros, |
| int? timeExtentMicros, |
| }) => |
| _getPerfettoVMTimeline(); |
| |
| Future<PerfettoTimeline> _getPerfettoVMTimeline() { |
| final perfettoTimeline = _timelineData; |
| if (perfettoTimeline == null) { |
| throw StateError('timelineData was not provided to FakeServiceManager'); |
| } |
| return Future.value(perfettoTimeline); |
| } |
| |
| @override |
| Future<Success> clearVMTimeline() => Future.value(Success()); |
| |
| @override |
| Future<ClassList> getClassList(String isolateId) { |
| return Future.value(_classList ?? ClassList(classes: [])); |
| } |
| |
| @override |
| Future<bool> isSocketProfilingAvailableWrapper(String isolateId) { |
| return Future.value(true); |
| } |
| |
| @override |
| Future<SocketProfilingState> socketProfilingEnabledWrapper( |
| String isolateId, [ |
| bool? enabled, |
| ]) { |
| if (enabled != null) { |
| return Future.value(SocketProfilingState(enabled: enabled)); |
| } |
| return Future.value( |
| SocketProfilingState(enabled: socketProfilingEnabledResult), |
| ); |
| } |
| |
| @override |
| Future<Success> clearSocketProfileWrapper(String isolateId) { |
| _socketProfile?.sockets.clear(); |
| return Future.value(Success()); |
| } |
| |
| @override |
| Future<SocketProfile> getSocketProfileWrapper(String isolateId) { |
| return Future.value(_socketProfile ?? SocketProfile(sockets: [])); |
| } |
| |
| void restoreFakeSockets() { |
| _socketProfile = SocketProfile(sockets: _startingSockets); |
| } |
| |
| @override |
| Future<HttpProfileRequest> getHttpProfileRequestWrapper( |
| String isolateId, |
| String id, |
| ) async { |
| final httpProfile = await getHttpProfileWrapper(isolateId); |
| return Future.value( |
| httpProfile.requests.firstWhere((request) => request.id == id), |
| ); |
| } |
| |
| @override |
| Future<HttpProfile> getHttpProfileWrapper( |
| String isolateId, { |
| DateTime? updatedSince, |
| }) { |
| return Future.value( |
| _httpProfile ?? |
| HttpProfile( |
| requests: [], |
| timestamp: DateTime.fromMicrosecondsSinceEpoch(0), |
| ), |
| ); |
| } |
| |
| @override |
| Future<Success> clearHttpProfileWrapper(String isolateId) { |
| _httpProfile?.requests.clear(); |
| return Future.value(Success()); |
| } |
| |
| void restoreFakeHttpProfileRequests() { |
| _httpProfile = HttpProfile( |
| requests: _startingRequests, |
| timestamp: DateTime.fromMicrosecondsSinceEpoch(0), |
| ); |
| } |
| |
| @override |
| Future<Success> clearCpuSamples(String isolateId) => Future.value(Success()); |
| |
| @override |
| Future<bool> isHttpTimelineLoggingAvailableWrapper(String isolateId) => |
| Future.value(isHttpProfilingAvailableResult); |
| |
| @override |
| Future<HttpTimelineLoggingState> httpEnableTimelineLoggingWrapper( |
| String isolateId, [ |
| bool? enabled, |
| ]) { |
| if (enabled != null) { |
| return Future.value(HttpTimelineLoggingState(enabled: enabled)); |
| } |
| return Future.value( |
| HttpTimelineLoggingState(enabled: httpEnableTimelineLoggingResult), |
| ); |
| } |
| |
| @override |
| Future<SourceReport> getSourceReport( |
| String isolateId, |
| List<String> reports, { |
| String? scriptId, |
| int? tokenPos, |
| int? endTokenPos, |
| bool? forceCompile, |
| bool? reportLines, |
| List<String>? libraryFilters, |
| List<String>? librariesAlreadyCompiled, |
| }) { |
| return Future.value(SourceReport(ranges: [], scripts: [])); |
| } |
| |
| @override |
| Future<ObjectStore?> getObjectStore(String isolateId) => Future.value( |
| const ObjectStore( |
| fields: {}, |
| ), |
| ); |
| |
| @override |
| final fakeServiceCache = JsonToServiceCache(); |
| |
| @override |
| Future<Timestamp> getVMTimelineMicros() => |
| Future.value(Timestamp(timestamp: 0)); |
| |
| @override |
| Stream<Event> onEvent(String streamName) => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onStdoutEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onStdoutEventWithHistorySafe => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onStderrEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onStderrEventWithHistorySafe => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onGCEvent => _gcEventStream.stream; |
| |
| void emitGCEvent() { |
| _gcEventStream.sink.add( |
| Event( |
| kind: EventKind.kGC, |
| ), |
| ); |
| } |
| |
| @override |
| Stream<Event> get onVMEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onLoggingEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onLoggingEventWithHistorySafe => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onExtensionEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onExtensionEventWithHistorySafe => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onDebugEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onTimelineEvent => const Stream.empty(); |
| |
| @override |
| Stream<Event> get onIsolateEvent => const Stream.empty(); |
| } |