blob: 64c3a4477481a976c95cf4462e72289ac14ed92b [file] [log] [blame] [edit]
// Copyright (c) 2022, 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:io';
import 'package:meta/meta.dart';
/// A utility class to get information about the Dart related process running on
/// this machine.
class ProcessInfo {
static final wsRegex = RegExp(r'\s+');
final String command;
final String commandLine;
final int memoryMb;
final double? cpuPercent;
final String? elapsedTime;
@visibleForTesting
static ProcessInfo parseMacos(String line, {bool elideFilePaths = true}) {
// "33712 0.0 01-19:07:19 launchd ..."
line = line.replaceAll(wsRegex, ' ');
String nextWord() {
var index = line.indexOf(' ');
var word = line.substring(0, index);
line = line.substring(index + 1);
return word;
}
var mb = nextWord();
var cpu = nextWord();
var elapsedTime = nextWord();
var commandLine = line.trim();
if (elideFilePaths) {
return ProcessInfo.create(
command: _getCommandFrom(commandLine),
commandLine: _sanitizeCommandLine(commandLine, preferSnapshot: true),
memoryMb: int.parse(mb) ~/ 1024,
cpuPercent: double.tryParse(cpu.replaceAll(',', '.')),
elapsedTime: elapsedTime,
);
} else {
return ProcessInfo.create(
command: _getCommandFrom(commandLine),
commandLine: commandLine,
memoryMb: int.parse(mb) ~/ 1024,
cpuPercent: double.tryParse(cpu.replaceAll(',', '.')),
elapsedTime: elapsedTime,
);
}
}
static ProcessInfo? _parseLinux(String line, {bool elideFilePaths = true}) {
// "33712 0.0 01-19:07:19 launchd ..."
line = line.replaceAll(wsRegex, ' ');
String nextWord() {
var index = line.indexOf(' ');
var word = line.substring(0, index);
line = line.substring(index + 1);
return word;
}
var mb = nextWord();
var cpu = nextWord();
var elapsedTime = nextWord();
var commandLine = line.trim();
if (commandLine.startsWith('[') && commandLine.endsWith(']')) {
return null;
}
if (elideFilePaths) {
return ProcessInfo.create(
command: _getCommandFrom(commandLine),
commandLine: _sanitizeCommandLine(commandLine, preferSnapshot: true),
memoryMb: int.parse(mb) ~/ 1024,
cpuPercent: double.tryParse(cpu.replaceAll(',', '.')),
elapsedTime: elapsedTime,
);
} else {
return ProcessInfo.create(
command: _getCommandFrom(commandLine),
commandLine: commandLine,
memoryMb: int.parse(mb) ~/ 1024,
cpuPercent: double.tryParse(cpu.replaceAll(',', '.')),
elapsedTime: elapsedTime,
);
}
}
@visibleForTesting
static ProcessInfo? parseWindows(String line) {
String stripQuotes(String item) {
if (item.startsWith('"')) item = item.substring(1);
if (item.endsWith('"')) item = item.substring(0, item.length - 1);
return item;
}
// "dart.exe","12068","Console","1","233,384 K"
var items = stripQuotes(line).split('","');
if (items.isEmpty) {
return null;
}
int parseMemory(String value) {
if (value.contains(' ')) value = value.substring(0, value.indexOf(' '));
value = value.replaceAll(',', '');
return (int.tryParse(value) ?? 0) ~/ 1024;
}
return ProcessInfo.create(
command: items.first,
commandLine: items.first,
memoryMb: parseMemory(items.length >= 5 ? items[4] : '0'),
cpuPercent: null,
elapsedTime: null,
);
}
factory ProcessInfo.create({
required String command,
required String commandLine,
required int memoryMb,
required double? cpuPercent,
required String? elapsedTime,
}) {
// Patch up the name if necessary (spaces in the path can throw off the
// parsers).
if (command.endsWith('.snapshot')) {
commandLine = '$command $commandLine'.trim();
command = 'dart';
}
return ProcessInfo._(
command: command,
commandLine: commandLine,
memoryMb: memoryMb,
cpuPercent: cpuPercent,
elapsedTime: elapsedTime,
);
}
const ProcessInfo._({
required this.command,
required this.commandLine,
required this.memoryMb,
required this.cpuPercent,
required this.elapsedTime,
});
/// Return the Dart related processes.
///
/// This will try to exclude the process for the VM currently running
/// 'dart info'.
///
/// This will return `null` if we don't support listing the process on the
/// current platform.
static List<ProcessInfo>? getProcessInfo({bool elideFilePaths = true}) {
List<ProcessInfo>? processInfo;
if (Platform.isMacOS) {
processInfo = _getProcessInfoMacOS(elideFilePaths: elideFilePaths);
} else if (Platform.isLinux) {
processInfo = _getProcessInfoLinux(elideFilePaths: elideFilePaths);
} else if (Platform.isWindows) {
processInfo = _getProcessInfoWindows();
}
if (processInfo != null) {
// Remove the 'dart info' entry.
processInfo = processInfo
.where((process) => process.commandLine != 'dart info')
.toList();
// Sort.
processInfo.sort((a, b) => a.commandLine.compareTo(b.commandLine));
}
return processInfo;
}
/// Return the given [commandLine] with path-like elements replaced with
/// shorter placeholders.
static String _sanitizeCommandLine(
String commandLine, {
bool preferSnapshot = true,
}) {
final sep = Platform.pathSeparator;
var args = commandLine.split(' ');
// If we're running 'dart foo.snapshot ...', and we're already adjusting
// the command line, shorten the command line so it appears that the
// snapshot is being run directly (some command lines can be very long
// otherwise).
var index = args.indexWhere((arg) => arg.endsWith('.snapshot'));
if (index != -1) {
args = args.skip(index).toList();
}
String sanitizeArg(String arg) {
if (!arg.contains(sep)) return arg;
int start = arg.indexOf(sep);
int end = arg.lastIndexOf(sep);
if (start == end) return arg;
return '${arg.substring(0, start)}<path>${arg.substring(end)}';
}
return [
args.first.split(sep).last,
...args.skip(1).map(sanitizeArg),
].join(' ');
}
@override
String toString() =>
'ProcessInfo(memoryMb: $memoryMb, cpuPercent: $cpuPercent, elapsedTime:'
' $elapsedTime, command: $command, commandLine: $commandLine)';
}
List<ProcessInfo> _getProcessInfoMacOS({bool elideFilePaths = true}) {
var result = Process.runSync('ps', ['-eo', 'rss,pcpu,etime,args']);
if (result.exitCode != 0) {
return const [];
}
// RSS %CPU ELAPSED ARGS
// 33712 0.0 01-19:07:19 launchd
// 52624 0.0 01-19:06:18 logd
// 4848 0.0 01-19:06:18 smd
var lines = (result.stdout as String).split('\n');
return lines
.skip(1)
.map((line) => line.trim())
.where((line) => line.isNotEmpty)
.map((line) =>
ProcessInfo.parseMacos(line, elideFilePaths: elideFilePaths))
.where(_isProcessDartRelated)
.toList();
}
List<ProcessInfo> _getProcessInfoLinux({bool elideFilePaths = true}) {
var result = Process.runSync('ps', ['-eo', 'rss,pcpu,etime,args']);
if (result.exitCode != 0) {
return const [];
}
var lines = (result.stdout as String).split('\n');
return lines
.skip(1)
.map((line) => line.trim())
.where((line) => line.isNotEmpty)
.map((line) =>
ProcessInfo._parseLinux(line, elideFilePaths: elideFilePaths))
.whereType<ProcessInfo>()
.where(_isProcessDartRelated)
.toList();
}
List<ProcessInfo> _getProcessInfoWindows() {
// TODO(devoncarew): Use tasklist /v to retrieve process elapsed time info.
var result = Process.runSync('tasklist', ['/nh', '/fo', 'csv']);
if (result.exitCode != 0) {
return const [];
}
// "smss.exe","608","Services","0","288 K"
// "csrss.exe","888","Services","0","3,084 K"
// "wininit.exe","628","Services","0","1,104 K"
// "dart.exe","12068","Console","1","233,384 K"
var lines = (result.stdout as String).split('\n');
return lines
.skip(1)
.map((line) => line.trim())
.where((line) => line.isNotEmpty)
.map((line) => ProcessInfo.parseWindows(line))
.whereType<ProcessInfo>()
.where(_isProcessDartRelated)
.toList();
}
bool _isProcessDartRelated(ProcessInfo process) {
return process.command == 'dart' || process.command == 'dart.exe' ||
process.command == 'dartvm' || process.command == 'dartvm.exe';
}
String _getCommandFrom(String commandLine) {
var command = commandLine.split(' ').first;
return command.split(Platform.pathSeparator).last;
}