From db06bb7f13a3c31135f9857f9f79a6657871cac6 Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Tue, 16 Dec 2025 12:30:25 -0800 Subject: [PATCH] Initialize SDK and regular projects on creation Runs either `dart pub get` or `gclient sync`, based on project type. For the SDK, copies the .gclient and .gclient_entries files as well into the parent dir. Also adds a `--timeout` argument, controls how long to wait for analyzer messages. Change-Id: Id2c28e6d0251e94914bb0650be104c1f90a66651 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/468620 Reviewed-by: Brian Wilkerson Commit-Queue: Jake Macdonald Auto-Submit: Jake Macdonald --- .../tool/log_player/log_player.dart | 16 ++++-- .../git_clone_project_generator.dart | 1 + .../git_worktree_project_generator.dart | 52 ++++++++++++++++++- .../project_generator/project_generator.dart | 25 +++++++++ .../scenarios/run_saved_scenarios.dart | 16 ++++-- .../tool/performance/scenarios/scenario.dart | 42 ++++++++++----- 6 files changed, 131 insertions(+), 21 deletions(-) diff --git a/pkg/analysis_server/tool/log_player/log_player.dart b/pkg/analysis_server/tool/log_player/log_player.dart index 983a3538af4..9c9cc2fde94 100644 --- a/pkg/analysis_server/tool/log_player/log_player.dart +++ b/pkg/analysis_server/tool/log_player/log_player.dart @@ -15,6 +15,13 @@ import 'package:collection/collection.dart'; import 'log.dart'; import 'server_driver.dart'; +/// Some messages from the analysis server should just be ignored. +bool _shouldSkip(Message message) => + // The server always sends this but we don't record it. + message.method == 'workspace/configuration' || + // This is the response to the initialize request. + message.id == 0; + /// An object used to play back the messages in a log. /// /// A reasonable attempt is made to retain the same timing of messages as was @@ -33,7 +40,10 @@ class LogPlayer { /// options from command line arguments. final driverArgParser = Driver.createArgParser(); - LogPlayer({required this.log}); + /// How long to wait for expected analyzer logs to come back. + final Duration timeout; + + LogPlayer({required this.log, this.timeout = const Duration(seconds: 5)}); /// Plays the log. Future play() async { @@ -73,7 +83,7 @@ class LogPlayer { actualServerMessageIds[foundMessage.id] = message.id; } pendingServerMessageExpectations.remove(foundMessage); - } else { + } else if (!_shouldSkip(message)) { stderr.writeln( 'Unexpected message from analysis server:\n' '${jsonEncode(message)}', @@ -171,7 +181,7 @@ receiver: ${entry.receiver} ) async { if (pendingServerMessageExpectations.isEmpty) return; var watch = Stopwatch()..start(); - while (watch.elapsed < const Duration(seconds: 5)) { + while (watch.elapsed < timeout) { if (pendingServerMessageExpectations.isEmpty) { return; } diff --git a/pkg/analysis_server/tool/performance/project_generator/git_clone_project_generator.dart b/pkg/analysis_server/tool/performance/project_generator/git_clone_project_generator.dart index b4f7cdd5c35..91da9f8ff07 100644 --- a/pkg/analysis_server/tool/performance/project_generator/git_clone_project_generator.dart +++ b/pkg/analysis_server/tool/performance/project_generator/git_clone_project_generator.dart @@ -27,6 +27,7 @@ class GitCloneProjectGenerator implements ProjectGenerator { await runGitCommand(['clone', repo, '.'], outputDir); await runGitCommand(['fetch', 'origin', ref], outputDir); await runGitCommand(['checkout', ref], outputDir); + await runPubGet(outputDir); return [outputDir]; } diff --git a/pkg/analysis_server/tool/performance/project_generator/git_worktree_project_generator.dart b/pkg/analysis_server/tool/performance/project_generator/git_worktree_project_generator.dart index 4cf6dbb9525..807cc4dd7b2 100644 --- a/pkg/analysis_server/tool/performance/project_generator/git_worktree_project_generator.dart +++ b/pkg/analysis_server/tool/performance/project_generator/git_worktree_project_generator.dart @@ -4,6 +4,8 @@ import 'dart:io'; +import 'package:path/path.dart' as p; + import '../utilities/git.dart'; import 'project_generator.dart'; @@ -16,7 +18,18 @@ class GitWorktreeProjectGenerator implements ProjectGenerator { /// The ref (commit sha, tag, or branch) to check out into a new working tree. final String ref; - GitWorktreeProjectGenerator(this.originalRepo, this.ref); + /// Whether or not this is an SDK repo. If it is, we use gclient and set things + /// up a bit differently. + final bool isSdkRepo; + + /// The root temp dir to clean up, if it isn't the same as the project dir. + Directory? tmpDir; + + GitWorktreeProjectGenerator( + this.originalRepo, + this.ref, { + this.isSdkRepo = false, + }); @override String get description => @@ -25,12 +38,27 @@ class GitWorktreeProjectGenerator implements ProjectGenerator { @override Future> setUp() async { var projectDir = await Directory.systemTemp.createTemp('as_git_worktree'); + if (isSdkRepo) { + if (tmpDir != null) { + throw StateError( + 'Project already set up, must wait for tearDown to complete to call ' + 'setUp again', + ); + } + tmpDir = projectDir; + projectDir = Directory(p.join(projectDir.path, 'sdk')); + } await runGitCommand([ 'worktree', 'add', '-d', projectDir.path, ], originalRepo); + if (isSdkRepo) { + await _setUpSdk(projectDir); + } else { + await runPubGet(projectDir); + } return [projectDir]; } @@ -45,5 +73,27 @@ class GitWorktreeProjectGenerator implements ProjectGenerator { '-f', workspaceDirs.single.path, ], originalRepo); + await tmpDir?.delete(recursive: true); + tmpDir = null; + } + + Future _setUpSdk(Directory projectDir) async { + print('Running gclient sync in ${projectDir.path}'); + var newGclientDir = p.dirname(projectDir.path); + var oldGclientDir = p.dirname(p.normalize(originalRepo.path)); + for (var file in ['.gclient', '.gclient_entries']) { + await File(p.join(oldGclientDir, file)).copy(p.join(newGclientDir, file)); + } + + var gclientSyncResult = await Process.run('gclient', [ + 'sync', + ], workingDirectory: projectDir.path); + if (gclientSyncResult.exitCode != 0) { + throw StateError( + 'Failed to run `gclient sync`:\n' + 'StdOut:\n${gclientSyncResult.stdout}\n' + 'StdErr:\n${gclientSyncResult.stderr}', + ); + } } } diff --git a/pkg/analysis_server/tool/performance/project_generator/project_generator.dart b/pkg/analysis_server/tool/performance/project_generator/project_generator.dart index 111f221660c..cbd0142dbed 100644 --- a/pkg/analysis_server/tool/performance/project_generator/project_generator.dart +++ b/pkg/analysis_server/tool/performance/project_generator/project_generator.dart @@ -4,6 +4,31 @@ import 'dart:io'; +import 'package:path/path.dart' as p; + +/// Runs `dart pub get` in [projectDir] if it contains a pubspec. +// +// TODO(jakemac): Support flutter projects and workspaces. +Future runPubGet(Directory projectDir) async { + var pubspec = File(p.join(p.normalize(projectDir.path), 'pubspec.yaml')); + if (pubspec.existsSync()) { + print('Fetching dependencies with pub in ${projectDir.path}'); + var pubGetResult = await Process.run('dart', [ + 'pub', + 'get', + ], workingDirectory: projectDir.path); + if (pubGetResult.exitCode != 0) { + throw StateError( + 'Failed to run `dart pub get`:\n' + 'StdOut:\n${pubGetResult.stdout}\n' + 'StdErr:\n${pubGetResult.stderr}', + ); + } + } else { + print('No pubspec.yaml found in ${projectDir.path}, skipping `pub get`'); + } +} + /// A [ProjectGenerator] represents a reproducible way to create a pristine /// copy of a codebase. /// diff --git a/pkg/analysis_server/tool/performance/scenarios/run_saved_scenarios.dart b/pkg/analysis_server/tool/performance/scenarios/run_saved_scenarios.dart index 475fe77bc68..012ee5b6811 100644 --- a/pkg/analysis_server/tool/performance/scenarios/run_saved_scenarios.dart +++ b/pkg/analysis_server/tool/performance/scenarios/run_saved_scenarios.dart @@ -23,7 +23,7 @@ void main(List args) async { if (scenarioNames.isNotEmpty && !scenarioNames.contains(scenario.name)) { continue; } - await scenario.run(); + await scenario.run(Duration(seconds: int.parse(parsed.option('timeout')!))); } } @@ -38,6 +38,12 @@ final argParser = ArgParser() help: 'The name(s) of specific scenario(s) to run', allowed: scenarios.map((s) => s.name).toList(), ) + ..addOption( + 'timeout', + abbr: 't', + help: 'Number of seconds to wait for analyzer responses', + defaultsTo: '5', + ) ..addFlag('help'); final logsRoot = analysisServerRoot.resolve( @@ -52,7 +58,11 @@ final List scenarios = () { logFile: fileSystem.getFile( logsRoot.resolve('sdk_rename_driver_class.json').toFilePath(), ), - project: GitWorktreeProjectGenerator(Directory.fromUri(sdkRoot), 'main'), + project: GitWorktreeProjectGenerator( + Directory.fromUri(sdkRoot), + 'main', + isSdkRepo: true, + ), ), Scenario( name: 'initialize', @@ -67,4 +77,4 @@ final List scenarios = () { ]; }(); -final sdkRoot = analysisServerRoot.resolve('../../'); +final sdkRoot = analysisServerRoot.resolve('../../../'); diff --git a/pkg/analysis_server/tool/performance/scenarios/scenario.dart b/pkg/analysis_server/tool/performance/scenarios/scenario.dart index be6c7969930..333746b6825 100644 --- a/pkg/analysis_server/tool/performance/scenarios/scenario.dart +++ b/pkg/analysis_server/tool/performance/scenarios/scenario.dart @@ -2,6 +2,7 @@ // 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:analyzer/file_system/file_system.dart'; @@ -16,42 +17,55 @@ final dartSdkRoot = p.dirname(p.dirname(Platform.resolvedExecutable)); /// A [Scenario] represents a combination of a [project] and a [logFile] to /// replay in that project. class Scenario { + /// Can be used on the command line to select this scenario. + /// + /// Should be lowercase with underscores and no spaces. final String name; + + /// The log file to replay for this scenario. final File logFile; + + /// Handles project setup. final ProjectGenerator project; Scenario({required this.name, required this.logFile, required this.project}); - Future run() async { + Future run(Duration timeout) async { var watch = Stopwatch()..start(); - void log(String message) { - print('${watch.elapsed}: $message'); - } + await runZoned( + () => _run(timeout), + zoneSpecification: ZoneSpecification( + print: (_, _, _, message) => + stdout.writeln('${watch.elapsed}: $message'), + ), + ); + } - log('Initializing scenario for project: ${project.description}'); + Future _run(Duration timeout) async { + print('Initializing scenario for project: ${project.description}'); - log('Setting up project'); + print('Setting up project'); var projectDirs = await project.setUp(); - log('Reading logs'); + print('Reading logs'); var logs = Log.fromFile(logFile, { for (var i = 0; i < projectDirs.length; i++) '{{workspaceFolder-$i}}': projectDirs.elementAt(i).path, '{{dartSdkRoot}}': dartSdkRoot, }); - log('Creating log player'); - var logPlayer = LogPlayer(log: logs); + print('Creating log player'); + var logPlayer = LogPlayer(log: logs, timeout: timeout); - log( + print( 'Scenario initialized with workpace dirs:\n' '${projectDirs.map((dir) => ' - ${dir.path}').join('\n')}', ); try { var scenarioWatch = Stopwatch()..start(); - log('Replaying scenario'); + print('Replaying scenario'); await logPlayer.play(); - log('Scenario completed, took ${scenarioWatch.elapsed} to replay'); + print('Scenario completed, took ${scenarioWatch.elapsed} to replay'); } catch (e, s) { print(''' Scenario failed with Error: $e @@ -60,9 +74,9 @@ StackTrace: $s '''); } finally { - log('Tearing down scenario for project'); + print('Tearing down scenario for project'); await project.tearDown(projectDirs); - log('Scenario cleaned up'); + print('Scenario cleaned up'); } } }