// 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 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; import 'dart:isolate' as isolate; import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { String isolateId; final TimelineObtainer timelineObtainer = TimelineObtainer(); setUpAll(() async { isolateId = developer.Service.getIsolateID(isolate.Isolate.current); final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); if (info.serverUri == null) { fail('This test _must_ be run with --enable-vmservice.'); } await timelineObtainer.connect(info.serverUri); await timelineObtainer.setDartFlags(); // Initialize the image cache. TestWidgetsFlutterBinding.ensureInitialized(); }); tearDownAll(() async { await timelineObtainer?.close(); }); test('Image cache tracing', () async { final TestImageStreamCompleter completer1 = TestImageStreamCompleter(); PaintingBinding.instance.imageCache.putIfAbsent( 'Test', () => completer1, ); PaintingBinding.instance.imageCache.clear(); final List> timelineEvents = await timelineObtainer.getTimelineData(); _expectTimelineEvents( timelineEvents, >[ { 'name': 'ImageCache.putIfAbsent', 'args': {'key': 'Test', 'isolateId': isolateId} }, { 'name': 'listener', 'args': {'parentId': '1', 'isolateId': isolateId} }, { 'name': 'ImageCache.clear', 'args': { 'pendingImages': 1, 'keepAliveImages': 0, 'liveImages': 1, 'currentSizeInBytes': 0, 'isolateId': isolateId, } }, ], ); }, skip: isBrowser); // uses dart:isolate and io } void _expectTimelineEvents( List> events, List> expected, ) { for (final Map event in events) { for (int index = 0; index < expected.length; index += 1) { if (expected[index]['name'] == event['name']) { final Map expectedArgs = expected[index]['args'] as Map; final Map args = event['args'] as Map; if (_mapsEqual(expectedArgs, args)) { expected.removeAt(index); } } } } if (expected.isNotEmpty) { final String encodedEvents = jsonEncode(events); fail('Timeline did not contain expected events: $expected\nactual: $encodedEvents'); } } bool _mapsEqual(Map expectedArgs, Map args) { for (final String key in expectedArgs.keys) { if (expectedArgs[key] != args[key]) { return false; } } return true; } // TODO(dnfield): we can drop this in favor of vm_service when https://github.com/dart-lang/webdev/issues/899 is resolved. class TimelineObtainer { WebSocket _observatorySocket; int _lastCallId = 0; final Map> _completers = >{}; Future connect(Uri uri) async { _observatorySocket = await WebSocket.connect('ws://localhost:${uri.port}${uri.path}ws'); _observatorySocket.listen((dynamic data) => _processResponse(data as String)); } void _processResponse(String data) { final Map json = jsonDecode(data) as Map; final int id = json['id'] as int; _completers.remove(id).complete(json['result']); } Future setDartFlags() async { _lastCallId += 1; final Completer> completer = Completer>(); _completers[_lastCallId] = completer; _observatorySocket.add(jsonEncode({ 'id': _lastCallId, 'method': 'setVMTimelineFlags', 'params': { 'recordedStreams': ['Dart'], }, })); final Map result = await completer.future; return result['type'] == 'Success'; } Future>> getTimelineData() async { _lastCallId += 1; final Completer> completer = Completer>(); _completers[_lastCallId] = completer; _observatorySocket.add(jsonEncode({ 'id': _lastCallId, 'method': 'getVMTimeline', })); final Map result = await completer.future; final List list = result['traceEvents'] as List; return list.cast>(); } Future close() async { expect(_completers, isEmpty); await _observatorySocket?.close(); } } class TestImageStreamCompleter extends ImageStreamCompleter {}