| // Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file | |
| // for details. 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:io'; | |
| import 'package:path/path.dart' as path; | |
| import '../benchmarks.dart'; | |
| import 'memory_tests.dart'; | |
| class FlutterCompletionBenchmark extends Benchmark implements FlutterBenchmark { | |
| static final das = FlutterCompletionBenchmark( | |
| 'das', | |
| AnalysisServerBenchmarkTest.new, | |
| ); | |
| static final lsp = FlutterCompletionBenchmark( | |
| 'lsp', | |
| LspAnalysisServerBenchmarkTest.new, | |
| ); | |
| final AbstractBenchmarkTest Function() testConstructor; | |
| late final String flutterRepositoryPath; | |
| new(String protocolName, this.testConstructor) | |
| : super( | |
| '$protocolName-flutter', | |
| 'Completion benchmarks with Flutter.', | |
| kind: 'group', | |
| ); | |
| @override | |
| int get maxIterations => 1; | |
| @override | |
| Future<BenchMarkResult> run({ | |
| required String dartSdkPath, | |
| bool quick = false, | |
| bool verbose = false, | |
| }) async { | |
| if (!quick) { | |
| deleteServerCache(); | |
| } | |
| var test = testConstructor(); | |
| if (verbose) { | |
| test.debugStdio(); | |
| } | |
| var flutterPkgPath = path.join( | |
| flutterRepositoryPath, | |
| 'packages', | |
| 'flutter', | |
| ); | |
| // Open a small directory, but with the package config that allows us | |
| // to analyze any file in `package:flutter`, including tests. | |
| var startTimer = Stopwatch()..start(); | |
| await test.setUp(dartSdkPath, ['$flutterPkgPath/lib/src/physics']); | |
| await test.analysisFinished; | |
| startTimer.stop(); | |
| var result = CompoundBenchMarkResult(id); | |
| result.add( | |
| 'start', | |
| BenchMarkResult('micros', startTimer.elapsedMicroseconds), | |
| ); | |
| // This is a scenario of an easy case - the file is small, less than | |
| // 3KB, and we insert a prefix with a good selectivity. So, everything | |
| // should be fast. We should just make sure to don't spend too much | |
| // time analyzing, and do apply the filter. | |
| // Total number of suggestions: 2322. | |
| // Filtered to: 82. | |
| // Long name: completion-smallFile-body | |
| var name = 'completion-1'; | |
| result.add( | |
| name, | |
| BenchMarkResult( | |
| 'micros', | |
| await _completionTiming( | |
| test, | |
| filePath: '$flutterPkgPath/lib/src/material/flutter_logo.dart', | |
| uniquePrefix: 'Widget build(BuildContext context) {', | |
| insertStringGenerator: () => 'M', | |
| name: name, | |
| ), | |
| ), | |
| ); | |
| if (!quick) { | |
| // This scenario is relatively easy - the file is small, less than 3KB. | |
| // But we don't have any prefix to filter, so if we don't restrict the | |
| // number of suggestions, we might spend too much time serializing into | |
| // JSON in the server, and deserializing on the client. | |
| // Total number of suggestions: 2322. | |
| // Filtered to: 2322. | |
| // Long name: completion-smallFile-body-withoutPrefix | |
| name = 'completion-2'; | |
| result.add( | |
| name, | |
| BenchMarkResult( | |
| 'micros', | |
| await _completionTiming( | |
| test, | |
| filePath: '$flutterPkgPath/lib/src/material/flutter_logo.dart', | |
| uniquePrefix: 'Widget build(BuildContext context) {', | |
| insertStringGenerator: null, | |
| name: name, | |
| ), | |
| ), | |
| ); | |
| // The key part of this scenario is that we work in a relatively large | |
| // file, about 340KB. So, it is expensive to parse and resolve. And | |
| // we simulate changing it by typing a prefix, as users often do. | |
| // The target method body is small, so something could be optimized. | |
| // Total number of suggestions: 4654. | |
| // Filtered to: 182. | |
| // Long name: completion-smallLibraryCycle-largeFile-smallBody | |
| name = 'completion-3'; | |
| result.add( | |
| name, | |
| BenchMarkResult( | |
| 'micros', | |
| await _completionTiming( | |
| test, | |
| filePath: '$flutterPkgPath/test/material/text_field_test.dart', | |
| uniquePrefix: 'getOpacity(WidgetTester tester, Finder finder) {', | |
| insertStringGenerator: () => 'M', | |
| name: name, | |
| ), | |
| ), | |
| ); | |
| // In this scenario we change a file that is in a library cycle | |
| // with 69 libraries. So, the implementation might discard information | |
| // about all these libraries. We change a method body, so the API | |
| // signature is the same, and we are able to reload these libraries | |
| // from bytes. But this still costs something. | |
| // Total number of suggestions: 3429. | |
| // Filtered to: 133. | |
| // Long name: completion-mediumLibraryCycle-mediumFile-smallBody | |
| name = 'completion-4'; | |
| result.add( | |
| name, | |
| BenchMarkResult( | |
| 'micros', | |
| await _completionTiming( | |
| test, | |
| filePath: '$flutterPkgPath/lib/src/material/app_bar.dart', | |
| uniquePrefix: 'computeDryLayout(BoxConstraints constraints) {', | |
| insertStringGenerator: () => 'M', | |
| name: name, | |
| ), | |
| ), | |
| ); | |
| // In this scenario is that we change a file that is in a library cycle | |
| // with 69 libraries. Moreover, we change the API - the type of a | |
| // formal parameter. So, potentially we need to relink the whole library | |
| // cycle. This is expensive. | |
| // Total number of suggestions: 1510. | |
| // Filtered to: 0. | |
| // Long name: completion-mediumLibraryCycle-mediumFile-api-parameterType | |
| name = 'completion-5'; | |
| result.add( | |
| name, | |
| BenchMarkResult( | |
| 'micros', | |
| await _completionTiming( | |
| test, | |
| filePath: '$flutterPkgPath/lib/src/material/app_bar.dart', | |
| uniquePrefix: '_handleScrollNotification(ScrollNotification', | |
| insertStringGenerator: _IncrementingStringGenerator().call, | |
| name: name, | |
| ), | |
| ), | |
| ); | |
| } | |
| await test.shutdown(); | |
| return result; | |
| } | |
| /// Perform completion in [filePath] at the end of the [uniquePrefix]. | |
| /// | |
| /// If [insertStringGenerator] is not `null`, insert it, and complete after | |
| /// it. So, we can simulate user typing to start completion. | |
| Future<int> _completionTiming( | |
| AbstractBenchmarkTest test, { | |
| required String filePath, | |
| required String uniquePrefix, | |
| required String Function()? insertStringGenerator, | |
| String? name, | |
| }) async { | |
| var fileContent = File(filePath).readAsStringSync(); | |
| var prefixOffset = fileContent.indexOf(uniquePrefix); | |
| if (prefixOffset == -1) { | |
| throw StateError('Cannot find: $uniquePrefix'); | |
| } | |
| if (fileContent.contains(uniquePrefix, prefixOffset + 1)) { | |
| throw StateError('Not unique: $uniquePrefix'); | |
| } | |
| var prefixEnd = prefixOffset + uniquePrefix.length; | |
| await test.openFile(filePath, fileContent); | |
| Future<void> perform({required bool isWarmUp}) async { | |
| var completionOffset = prefixEnd; | |
| if (insertStringGenerator != null) { | |
| var insertString = insertStringGenerator(); | |
| completionOffset += insertString.length; | |
| var newCode = | |
| fileContent.substring(0, prefixEnd) + | |
| insertString + | |
| fileContent.substring(prefixEnd); | |
| await test.updateFile(filePath, newCode); | |
| } | |
| await test.complete(filePath, completionOffset, isWarmUp: isWarmUp); | |
| if (insertStringGenerator != null) { | |
| await test.updateFile(filePath, fileContent); | |
| } | |
| } | |
| // Perform warm-up. | |
| // The cold start does not matter. | |
| // The sustained performance is much more important. | |
| const warmUpCount = 5; | |
| for (var i = 0; i < warmUpCount; i++) { | |
| await perform(isWarmUp: true); | |
| } | |
| const repeatCount = 5; | |
| var timer = Stopwatch()..start(); | |
| for (var i = 0; i < repeatCount; i++) { | |
| await perform(isWarmUp: false); | |
| } | |
| await test.closeFile(filePath); | |
| return timer.elapsedMicroseconds ~/ repeatCount; | |
| } | |
| } | |
| class _IncrementingStringGenerator { | |
| int _value = 0; | |
| String call() { | |
| return '${_value++}'; | |
| } | |
| } |