|
| 1 | +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file |
| 2 | +// for details. All rights reserved. Use of this source code is governed by a |
| 3 | +// BSD-style license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +import 'package:analyzer/file_system/overlay_file_system.dart'; |
| 6 | +import 'package:analyzer/file_system/physical_file_system.dart'; |
| 7 | +import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart'; |
| 8 | +import 'package:analyzer/src/dart/analysis/byte_store.dart'; |
| 9 | +import 'package:analyzer/src/dart/analysis/driver.dart'; |
| 10 | +import 'package:analyzer/src/dart/analysis/file_content_cache.dart'; |
| 11 | +import 'package:analyzer/src/util/performance/operation_performance.dart'; |
| 12 | +import 'package:collection/collection.dart'; |
| 13 | +import 'package:linter/src/rules.dart'; |
| 14 | + |
| 15 | +void main() async { |
| 16 | + const repeatCount = 2; |
| 17 | + const planRepeatCount = 2; |
| 18 | + var planCollection = planCollectionFlutter; |
| 19 | + |
| 20 | + registerLintRules(); |
| 21 | + |
| 22 | + var updateIndex = 0; |
| 23 | + for (var withFine in List.filled(repeatCount, [false, true]).flattened) { |
| 24 | + var resourceProvider = OverlayResourceProvider( |
| 25 | + PhysicalResourceProvider.INSTANCE, |
| 26 | + ); |
| 27 | + |
| 28 | + var collection = AnalysisContextCollectionImpl( |
| 29 | + resourceProvider: resourceProvider, |
| 30 | + sdkPath: Paths.sdkRun, |
| 31 | + includedPaths: planCollection.includedPaths, |
| 32 | + byteStore: MemoryByteStore(), |
| 33 | + fileContentCache: FileContentCache(resourceProvider), |
| 34 | + withFineDependencies: withFine, |
| 35 | + ); |
| 36 | + |
| 37 | + // Add all Dart files. |
| 38 | + for (var analysisContext in collection.contexts) { |
| 39 | + for (var path in analysisContext.contextRoot.analyzedFiles()) { |
| 40 | + if (path.endsWith('.dart')) { |
| 41 | + analysisContext.driver.addFile(path); |
| 42 | + } |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + var initialAnalysisTimer = Stopwatch()..start(); |
| 47 | + collection.scheduler.resetAccumulatedPerformance(); |
| 48 | + await collection.scheduler.waitForIdle(); |
| 49 | + await pumpEventQueue(); |
| 50 | + initialAnalysisTimer.stop(); |
| 51 | + (withFine |
| 52 | + ? planCollection.initialAnalysisWithFineTrue |
| 53 | + : planCollection.initialAnalysisWithFineFalse) |
| 54 | + .add(initialAnalysisTimer.elapsed); |
| 55 | + |
| 56 | + { |
| 57 | + print('\n' * 3); |
| 58 | + print( |
| 59 | + '[withFine: $withFine] Initial analysis, ' |
| 60 | + '${initialAnalysisTimer.elapsedMilliseconds} ms', |
| 61 | + ); |
| 62 | + print('-' * 64); |
| 63 | + |
| 64 | + var buffer = StringBuffer(); |
| 65 | + collection.scheduler.accumulatedPerformance.write(buffer: buffer); |
| 66 | + print(buffer.toString().trim()); |
| 67 | + } |
| 68 | + |
| 69 | + for (var plan in planCollection.plans) { |
| 70 | + var targetPath = plan.filePath; |
| 71 | + var targetCode = PhysicalResourceProvider.INSTANCE |
| 72 | + .getFile(targetPath) |
| 73 | + .readAsStringSync(); |
| 74 | + for (var i = 0; i < planRepeatCount; i++) { |
| 75 | + // Update. |
| 76 | + var replacement = plan.replacementTemplate.replaceAll( |
| 77 | + '#[UI]', |
| 78 | + '${updateIndex++}', |
| 79 | + ); |
| 80 | + resourceProvider.setOverlay( |
| 81 | + targetPath, |
| 82 | + content: targetCode.replaceAll(plan.searchText, replacement), |
| 83 | + modificationStamp: 0, |
| 84 | + ); |
| 85 | + for (var analysisContext in collection.contexts) { |
| 86 | + analysisContext.changeFile(targetPath); |
| 87 | + } |
| 88 | + |
| 89 | + // Measure. |
| 90 | + var timer = Stopwatch()..start(); |
| 91 | + collection.scheduler.resetAccumulatedPerformance(); |
| 92 | + await collection.scheduler.waitForIdle(); |
| 93 | + await pumpEventQueue(); |
| 94 | + { |
| 95 | + print('\n' * 3); |
| 96 | + print('[withFine: $withFine][$i] $targetPath'); |
| 97 | + print(' searchText: ${plan.searchText}'); |
| 98 | + print(' replacement: $replacement'); |
| 99 | + var elapsed = timer.elapsed; |
| 100 | + print(' timer: ${elapsed.inMilliseconds} ms'); |
| 101 | + print('-' * 64); |
| 102 | + |
| 103 | + (withFine ? plan.withFineTrue : plan.withFineFalse).add(elapsed); |
| 104 | + |
| 105 | + var buffer = StringBuffer(); |
| 106 | + collection.scheduler.accumulatedPerformance.write(buffer: buffer); |
| 107 | + print(buffer.toString().trim()); |
| 108 | + } |
| 109 | + |
| 110 | + // Revert. |
| 111 | + { |
| 112 | + resourceProvider.setOverlay( |
| 113 | + targetPath, |
| 114 | + content: targetCode, |
| 115 | + modificationStamp: 1, |
| 116 | + ); |
| 117 | + for (var analysisContext in collection.contexts) { |
| 118 | + analysisContext.changeFile(targetPath); |
| 119 | + } |
| 120 | + await collection.scheduler.waitForIdle(); |
| 121 | + await pumpEventQueue(); |
| 122 | + print('-' * 32); |
| 123 | + print('[reverted][waitForIdle]'); |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + print('\n' * 3); |
| 130 | + print('${'-' * 64} results'); |
| 131 | + _printDurations( |
| 132 | + 'Initial analysis', |
| 133 | + planCollection.initialAnalysisWithFineFalse, |
| 134 | + planCollection.initialAnalysisWithFineTrue, |
| 135 | + ); |
| 136 | + print(''); |
| 137 | + |
| 138 | + for (var plan in planCollection.plans) { |
| 139 | + _printDurations(plan.filePath, plan.withFineFalse, plan.withFineTrue); |
| 140 | + print(''); |
| 141 | + } |
| 142 | + print('\n' * 2); |
| 143 | +} |
| 144 | + |
| 145 | +final planCollectionAnalyzer = PlanCollection( |
| 146 | + includedPaths: [Paths.sdkAnalyzer], |
| 147 | + plans: [ |
| 148 | + Plan( |
| 149 | + filePath: '${Paths.sdkAnalyzer}/lib/src/fine/library_manifest.dart', |
| 150 | + searchText: 'computeManifests({', |
| 151 | + replacementTemplate: 'computeManifests#[UI]({', |
| 152 | + ), |
| 153 | + ], |
| 154 | +); |
| 155 | + |
| 156 | +final planCollectionFlutter = PlanCollection( |
| 157 | + includedPaths: [Paths.flutterPackage], |
| 158 | + plans: [ |
| 159 | + Plan( |
| 160 | + filePath: |
| 161 | + '${Paths.flutterPackage}/lib/src/foundation/memory_allocations.dart', |
| 162 | + searchText: 'dispatchObjectEvent(ObjectEvent event) {', |
| 163 | + replacementTemplate: 'dispatchObjectEvent#[UI](ObjectEvent event) {', |
| 164 | + ), |
| 165 | + Plan( |
| 166 | + filePath: '${Paths.flutterPackage}/lib/src/painting/image_cache.dart', |
| 167 | + searchText: 'containsKey(Object key) {', |
| 168 | + replacementTemplate: 'containsKey#[UI](Object key) {', |
| 169 | + ), |
| 170 | + Plan( |
| 171 | + filePath: '${Paths.flutterPackage}/lib/src/widgets/banner.dart', |
| 172 | + searchText: 'shouldRepaint(BannerPainter oldDelegate) {', |
| 173 | + replacementTemplate: 'shouldRepaint#[UI](BannerPainter oldDelegate) {', |
| 174 | + ), |
| 175 | + ], |
| 176 | +); |
| 177 | + |
| 178 | +Future pumpEventQueue([int times = 5000]) { |
| 179 | + if (times == 0) return Future.value(); |
| 180 | + return Future.delayed(Duration.zero, () => pumpEventQueue(times - 1)); |
| 181 | +} |
| 182 | + |
| 183 | +String _formatFineDelta( |
| 184 | + Durations fineFalseDurations, |
| 185 | + Durations fineTrueDurations, |
| 186 | +) { |
| 187 | + var baseMs = fineFalseDurations.best.inMilliseconds; |
| 188 | + var fineMs = fineTrueDurations.best.inMilliseconds; |
| 189 | + |
| 190 | + if (baseMs == 0 && fineMs == 0) { |
| 191 | + return ' fine-grained: undefined (time $baseMs → $fineMs ms)'; |
| 192 | + } |
| 193 | + |
| 194 | + var deltaMs = fineMs - baseMs; |
| 195 | + if (deltaMs == 0) { |
| 196 | + return ' fine-grained: no change (time $baseMs → $fineMs ms)'; |
| 197 | + } |
| 198 | + |
| 199 | + var percent = (deltaMs / baseMs) * 100; |
| 200 | + var percentStr = '${percent >= 0 ? '+' : ''}${percent.toStringAsFixed(1)}%'; |
| 201 | + var timeStr = 'time $baseMs → $fineMs ms'; |
| 202 | + |
| 203 | + if (fineMs < baseMs) { |
| 204 | + var ratio = baseMs / fineMs; |
| 205 | + return ' fine-grained: ${ratio.toStringAsFixed(1)}× faster ' |
| 206 | + '($timeStr; $percentStr)'; |
| 207 | + } else { |
| 208 | + var ratio = fineMs / baseMs; |
| 209 | + return ' fine-grained: ${ratio.toStringAsFixed(1)}× slower ' |
| 210 | + '($timeStr; $percentStr)'; |
| 211 | + } |
| 212 | +} |
| 213 | + |
| 214 | +void _printDurations( |
| 215 | + String title, |
| 216 | + Durations fineFalseDurations, |
| 217 | + Durations fineTrueDurations, |
| 218 | +) { |
| 219 | + print(title); |
| 220 | + print(fineFalseDurations.format('[withFine: false]')); |
| 221 | + print(fineTrueDurations.format('[withFine: true ]')); |
| 222 | + print(_formatFineDelta(fineFalseDurations, fineTrueDurations)); |
| 223 | +} |
| 224 | + |
| 225 | +class Durations { |
| 226 | + final List<Duration> values = []; |
| 227 | + |
| 228 | + Duration get best { |
| 229 | + if (values.isEmpty) { |
| 230 | + return Duration.zero; |
| 231 | + } |
| 232 | + return values.min; |
| 233 | + } |
| 234 | + |
| 235 | + void add(Duration value) { |
| 236 | + values.add(value); |
| 237 | + } |
| 238 | + |
| 239 | + String format(String title) { |
| 240 | + return ' $title, ' |
| 241 | + 'best: ${best.inMilliseconds} ms, ' |
| 242 | + 'all: ${values.map((e) => e.inMilliseconds).toList()}'; |
| 243 | + } |
| 244 | +} |
| 245 | + |
| 246 | +class Paths { |
| 247 | + static const sdkRun = '/Users/scheglov/Applications/dart-sdk'; |
| 248 | + |
| 249 | + static const sdkRepo = '/Users/scheglov/Source/Dart/sdk.git/sdk'; |
| 250 | + static const sdkAnalyzer = '$sdkRepo/pkg/analyzer'; |
| 251 | + static const sdkAnalysisServer = '$sdkRepo/pkg/analysis_server'; |
| 252 | + static const sdkLinter = '$sdkRepo/pkg/linter'; |
| 253 | + |
| 254 | + static const flutterRepo = '/Users/scheglov/Source/flutter'; |
| 255 | + static const flutterPackage = '$flutterRepo/packages/flutter'; |
| 256 | +} |
| 257 | + |
| 258 | +class Plan { |
| 259 | + final String filePath; |
| 260 | + final String searchText; |
| 261 | + final String replacementTemplate; |
| 262 | + |
| 263 | + final Durations withFineFalse = Durations(); |
| 264 | + final Durations withFineTrue = Durations(); |
| 265 | + |
| 266 | + Plan({ |
| 267 | + required this.filePath, |
| 268 | + required this.searchText, |
| 269 | + required this.replacementTemplate, |
| 270 | + }); |
| 271 | +} |
| 272 | + |
| 273 | +class PlanCollection { |
| 274 | + final List<String> includedPaths; |
| 275 | + final List<Plan> plans; |
| 276 | + |
| 277 | + final Durations initialAnalysisWithFineFalse = Durations(); |
| 278 | + final Durations initialAnalysisWithFineTrue = Durations(); |
| 279 | + |
| 280 | + PlanCollection({required this.includedPaths, required this.plans}); |
| 281 | +} |
| 282 | + |
| 283 | +extension AnalysisDriverSchedulerPerformance on AnalysisDriverScheduler { |
| 284 | + /// Reset the accumulated scheduler performance to a fresh operation. |
| 285 | + void resetAccumulatedPerformance() { |
| 286 | + accumulatedPerformance = OperationPerformanceImpl('<scheduler>'); |
| 287 | + } |
| 288 | +} |
0 commit comments