blob: f029f136dc85e0da5b5657326fe53af9a811b391 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Some notes about filtering `adb logcat` output, especially as a result of
/// running `adb shell` to instrument the app and test scripts, as it's
/// non-trivial and error-prone.
///
/// 1. It's probably worth keeping `ActivityManager` lines unconditionally.
/// They are the most important ones, and they are not too verbose (for
/// example, they don't typically contain stack traces).
///
/// 2. `ActivityManager` starts with the application name and process ID:
///
/// ```txt
/// [stdout] 02-15 10:20:36.914 1735 1752 I ActivityManager: Start proc 6840:dev.flutter.scenarios/u0a98 for added application dev.flutter.scenarios
/// ```
///
/// The "application" comes from the file `android/app/build.gradle` under
/// `android > defaultConfig > applicationId`.
///
/// 3. Once we have the process ID, we can filter the logcat output further:
///
/// ```txt
/// [stdout] 02-15 10:20:37.430 6840 6840 E GeneratedPluginsRegister: Tried to automatically register plugins with FlutterEngine (io.flutter.embedding.engine.FlutterEngine@144d737) but could not find or invoke the GeneratedPluginRegistrant.
/// ```
///
/// A sample output of `adb logcat` command lives in `./sample_adb_logcat.txt`.
///
/// See also: <https://developer.android.com/tools/logcat>.
library;
import 'package:meta/meta.dart';
import 'logs.dart';
/// Represents a line of `adb logcat` output parsed into a structured form.
///
/// For example the line:
/// ```txt
/// 02-22 13:54:39.839 549 3683 I ActivityManager: Force stopping dev.flutter.scenarios appid=10226 user=0: start instr
/// ```
///
/// ## Implementation notes
///
/// The reason this is an extension type and not a class is partially to use the
/// language feature, and partially because extension types work really well
/// with lazy parsing.
extension type const AdbLogLine._(Match _match) {
// RegEx that parses into the following groups:
// 1. The time of the log message, such as `02-22 13:54:39.839`.
// 2. The process ID.
// 3. The thread ID.
// 4. The character representing the severity of the log message, such as `I`.
// 5. The tag, such as `ActivityManager`.
// 6. The actual log message.
//
// This regex is simple versus being more precise. Feel free to improve it.
static final RegExp _pattern = RegExp(r'(\d+-\d+\s[\d|:]+\.\d+)\s+(\d+)\s+(\d+)\s(\w)\s(\S+)\s*:\s*(.*)');
/// Parses the given [adbLogCatLine] into a structured form.
///
/// Returns `null` if the line does not match the expected format.
static AdbLogLine? tryParse(String adbLogCatLine) {
final Match? match = _pattern.firstMatch(adbLogCatLine);
return match == null ? null : AdbLogLine._(match);
}
/// Tries to parse the process that was started, if the log line is about it.
String? tryParseProcess() {
if (name == activityManagerTag && message.startsWith('Start proc')) {
// ActivityManager: Start proc 4475:dev.flutter.scenarios/u0a190 for added application ...
final RegExpMatch? match = RegExp('Start proc (\\d+):$flutterProcessName').firstMatch(message);
return match?.group(1);
}
return null;
}
@visibleForTesting
static const String activityManagerTag = 'ActivityManager';
@visibleForTesting
static const String flutterProcessName = 'dev.flutter.scenarios';
@visibleForTesting
static const Set<String> knownNoiseTags = <String>{
'CCodec',
'CCodecBufferChannel',
'CCodecConfig',
'Codec2Client',
'ColorUtils',
'DMABUFHEAPS',
'Gralloc4',
'MediaCodec',
'MonitoringInstr',
'ResourceExtractor',
'UsageTrackerFacilitator',
'hw-BpHwBinder',
'ziparchive',
};
@visibleForTesting
static const Set<String> knownUsefulTags = <String>{
activityManagerTag
};
@visibleForTesting
static const Set<String> knownUsefulErrorTags = <String>{
'androidemu',
'THREAD_STATE',
};
/// Returns `true` if the log line is verbose.
bool isVerbose({String? filterProcessId}) => !_isRelevant(filterProcessId: filterProcessId);
bool _isRelevant({String? filterProcessId}) {
// Fatal errors are always useful.
if (severity == 'F') {
return true;
}
// Verbose and debug logs are rarely useful.
if (severity == 'V' || severity == 'D') {
return false;
}
if (knownNoiseTags.contains(name)) {
return false;
}
if (knownUsefulTags.contains(name)) {
return true;
}
if (severity == 'E' && knownUsefulErrorTags.contains(name)) {
return true;
}
// If a process ID is specified, exclude logs _not_ from that process.
if (filterProcessId == null) {
// YOLO, let's keep it anyway.
return name.toLowerCase().contains('flutter') ||
message.toLowerCase().contains('flutter');
}
return process == filterProcessId;
}
/// Logs the line to the console.
void printFormatted() {
final String formatted = '$time [$severity] $name: $message';
if (severity == 'W' || severity == 'E' || severity == 'F') {
logWarning(formatted);
} else if (name == 'TestRunner') {
logImportant(formatted);
} else {
log(formatted);
}
}
/// The full line of `adb logcat` output.
String get line => _match.group(0)!;
/// The time of the log message, such as `02-22 13:54:39.839`.
String get time => _match.group(1)!;
/// The process ID.
String get process => _match.group(2)!;
/// The thread ID.
String get thread => _match.group(3)!;
/// The character representing the severity of the log message, such as `I`.
String get severity => _match.group(4)!;
/// The tag, such as `ActivityManager`.
String get name => _match.group(5)!;
/// The actual log message.
String get message => _match.group(6)!;
String toDebugString() {
return 'AdbLogLine(time: $time, process: $process, thread: $thread, severity: $severity, name: $name, message: $message)';
}
}