// Copyright (c) 2016, 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. /// An entrypoint used to run portions of analyzer and measure its performance. library; import 'dart:io' show exit; import 'package:analyzer/dart/analysis/features.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/error/listener.dart'; import 'package:analyzer/file_system/file_system.dart' show Folder; import 'package:analyzer/file_system/physical_file_system.dart' show PhysicalResourceProvider; import 'package:analyzer/source/line_info.dart'; import 'package:analyzer/source/source.dart'; import 'package:analyzer/src/context/packages.dart'; import 'package:analyzer/src/dart/analysis/experiments.dart'; import 'package:analyzer/src/dart/scanner/reader.dart'; import 'package:analyzer/src/dart/scanner/scanner.dart'; import 'package:analyzer/src/dart/sdk/sdk.dart' show FolderBasedDartSdk; import 'package:analyzer/src/file_system/file_system.dart'; import 'package:analyzer/src/generated/parser.dart'; import 'package:analyzer/src/generated/source.dart' show DartUriResolver, SourceFactory; import 'package:analyzer/src/source/package_map_resolver.dart'; import 'package:pub_semver/pub_semver.dart'; void main(List args) async { // TODO(sigmund): provide sdk folder as well. if (args.length < 2) { print('usage: perf.dart '); exit(1); } var totalTimer = Stopwatch()..start(); var bench = args[0]; var entryUri = Uri.base.resolve(args[1]); await setup(args[1]); if (bench == 'scan') { var files = scanReachableFiles(entryUri); // TODO(sigmund): consider replacing the warmup with instrumented snapshots. for (var i = 0; i < 10; i++) { scanFiles(files); } } else if (bench == 'parse') { var files = scanReachableFiles(entryUri); // TODO(sigmund): consider replacing the warmup with instrumented snapshots. for (var i = 0; i < 10; i++) { parseFiles(files); } } else { print('unsupported bench-id: $bench. Please specify "scan" or "parse"'); // TODO(sigmund): implement the remaining benchmarks. exit(1); } totalTimer.stop(); report('total', totalTimer.elapsedMicroseconds); } /// Cumulative time spent scanning. Stopwatch scanTimer = Stopwatch(); /// Cumulative total number of chars scanned. int scanTotalChars = 0; /// Factory to load and resolve app, packages, and sdk sources. late SourceFactory sources; /// Add to [files] all sources reachable from [start]. void collectSources(Source? start, Set files) { if (!files.add(start)) return; var unit = parseDirectives(start!); for (var directive in unit.directives) { if (directive is UriBasedDirective) { var next = sources.resolveUri(start, directive.uri.stringValue); collectSources(next, files); } } } /// Uses the diet-parser to parse only directives in [source]. CompilationUnit parseDirectives(Source source) { var result = tokenize(source); var languageVersion = LibraryLanguageVersion( package: ExperimentStatus.currentVersion, override: result.overrideVersion, ); var lineInfo = LineInfo(result.lineStarts); var parser = Parser( source, AnalysisErrorListener.NULL_LISTENER, featureSet: FeatureSet.latestLanguageVersion(), lineInfo: lineInfo, languageVersion: languageVersion, ); return parser.parseDirectives(result.tokens); } /// Parses every file in [files] and reports the time spent doing so. void parseFiles(Set files) { // The code below will record again how many chars are scanned and how long it // takes to scan them, even though we already did so in [scanReachableFiles]. // Recording and reporting this twice is unnecessary, but we do so for now to // validate that the results are consistent. scanTimer = Stopwatch(); var old = scanTotalChars; scanTotalChars = 0; var parseTimer = Stopwatch()..start(); for (var source in files) { parseFull(source!); } parseTimer.stop(); // Report size and scanning time again. See discussion above. if (old != scanTotalChars) print('input size changed? $old chars'); report('scan', scanTimer.elapsedMicroseconds); var pTime = parseTimer.elapsedMicroseconds - scanTimer.elapsedMicroseconds; report('parse', pTime); } /// Parse the full body of [source] and return it's compilation unit. CompilationUnit parseFull(Source source) { var result = tokenize(source); var languageVersion = LibraryLanguageVersion( package: ExperimentStatus.currentVersion, override: result.overrideVersion, ); var lineInfo = LineInfo(result.lineStarts); var parser = Parser( source, AnalysisErrorListener.NULL_LISTENER, featureSet: FeatureSet.latestLanguageVersion(), languageVersion: languageVersion, lineInfo: lineInfo, ); return parser.parseCompilationUnit(result.tokens); } /// Report that metric [name] took [time] micro-seconds to process /// [scanTotalChars] characters. void report(String name, int time) { var sb = StringBuffer(); sb.write('$name: $time us, ${time ~/ 1000} ms'); sb.write(', ${scanTotalChars * 1000 ~/ time} chars/ms'); print('$sb'); } /// Scans every file in [files] and reports the time spent doing so. void scanFiles(Set files) { // The code below will record again how many chars are scanned and how long it // takes to scan them, even though we already did so in [scanReachableFiles]. // Recording and reporting this twice is unnecessary, but we do so for now to // validate that the results are consistent. scanTimer = Stopwatch(); var old = scanTotalChars; scanTotalChars = 0; for (var source in files) { tokenize(source!); } // Report size and scanning time again. See discussion above. if (old != scanTotalChars) print('input size changed? $old chars'); report('scan', scanTimer.elapsedMicroseconds); } /// Load and scans all files we need to process: files reachable from the /// entrypoint and all core libraries automatically included by the VM. Set scanReachableFiles(Uri entryUri) { var files = {}; var loadTimer = Stopwatch()..start(); collectSources(sources.forUri2(entryUri), files); var libs = [ 'dart:async', 'dart:cli', 'dart:collection', 'dart:convert', 'dart:core', 'dart:developer', 'dart:_internal', 'dart:isolate', 'dart:math', 'dart:mirrors', 'dart:typed_data', 'dart:io', ]; for (var lib in libs) { collectSources(sources.forUri(lib), files); } loadTimer.stop(); print('input size: $scanTotalChars chars'); var loadTime = loadTimer.elapsedMicroseconds - scanTimer.elapsedMicroseconds; report('load', loadTime); report('scan', scanTimer.elapsedMicroseconds); return files; } /// Sets up analyzer to be able to load and resolve app, packages, and sdk /// sources. Future setup(String path) async { var provider = PhysicalResourceProvider.INSTANCE; var packages = findPackagesFrom(provider, provider.getResource(path)); var packageMap = >{}; for (var package in packages.packages) { packageMap[package.name] = [package.libFolder]; } sources = SourceFactory([ ResourceUriResolver(provider), PackageMapUriResolver(provider, packageMap), DartUriResolver(FolderBasedDartSdk(provider, provider.getFolder('sdk'))), ]); } /// Scan [source] and return the first token produced by the scanner. ScannerResult tokenize(Source source) { scanTimer.start(); var contents = source.contents.data; scanTotalChars += contents.length; // TODO(paulberry): figure out the appropriate featureSet to use here var featureSet = FeatureSet.latestLanguageVersion(); // TODO(sigmund): is there a way to scan from a random-access-file without // first converting to String? var scanner = Scanner( source, CharSequenceReader(contents), AnalysisErrorListener.NULL_LISTENER, ) ..configureFeatures( featureSetForOverriding: featureSet, featureSet: featureSet, ) ..preserveComments = false; var token = scanner.tokenize(); scanTimer.stop(); return ScannerResult(token, scanner.lineStarts, scanner.overrideVersion); } class ScannerResult { final Token tokens; final List lineStarts; final Version? overrideVersion; ScannerResult(this.tokens, this.lineStarts, this.overrideVersion); }