User/ivberg/logcat improve durations (#71)

* Improve logcat durations for retail logs by adding - SurfaceFlinger Boot, Looper, and OpenGLRender durations
* Add some more retail log improvements seen in Android12
* Logcat - Support for handling optional year + bug fixes
This commit is contained in:
ivberg 2022-01-05 12:04:40 -08:00 коммит произвёл GitHub
Родитель f8423bfb3b
Коммит efa848b468
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 146 добавлений и 30 удалений

Просмотреть файл

@ -26,10 +26,11 @@ namespace LinuxLogParser.AndroidLogcat
/// Per https://developer.android.com/studio/debug/am-logcat
/// date time PID TID priority tag: message
/// Example: "12-13 10:32:24.869 26 26 I Checkpoint: cp_prepareCheckpoint called"
public static Regex AndroidLogCatRegex = new Regex(@"^(.{18})\s+(\d+)\s+(\d+)\s+(\S) (.+?)\s?: (.*)$");
public static Regex AndroidLogCatRegex = new Regex(@"^([0-9-]+ [0-9:.]+)\s+(\d+)\s+(\d+)\s+(\S) (.+?)\s?: (.*)$");
const int SECONDS_TO_NS = 1000000000;
const int MS_TO_NS = 1000000;
const long SECONDS_TO_NS = 1000000000;
const long MS_TO_NS = 1000000;
static readonly TimestampDelta OneNanoSecondTimestampDelta = new TimestampDelta(1); // At least 1ns timestamp for duration
public AndroidLogcatLogParser(string[] filePaths) : base(filePaths)
{
@ -45,7 +46,6 @@ namespace LinuxLogParser.AndroidLogcat
long startNanoSeconds = 0;
DateTime fileStartTime = default;
var dateTimeCultureInfo = new CultureInfo("en-US");
var oneNanoSecondTimestampDelta = new TimestampDelta(1); // At least 1ns timestamp for duration
double? utcOffsetInHours = null;
foreach (var path in FilePaths)
@ -57,6 +57,8 @@ namespace LinuxLogParser.AndroidLogcat
var durationLogEntries = new List<DurationLogEntry>();
LogEntry logEntry = null;
string timeDateFormat = null;
var rootFolder = Path.GetDirectoryName(path);
var utcOffsetFilePath = Path.Combine(rootFolder, "utcoffset.txt");
if (File.Exists(utcOffsetFilePath))
@ -79,10 +81,28 @@ namespace LinuxLogParser.AndroidLogcat
var androidLogCatMatch = AndroidLogCatRegex.Match(line);
if (androidLogCatMatch.Success && timeDateFormat == null)
{
const string monthDayFormat = "MM-dd HH:mm:ss.fff";
const string monthDayYearFormat = "MM-dd-yyyy HH:mm:ss.fff";
if (DateTime.TryParseExact(androidLogCatMatch.Groups[1].Value, monthDayFormat, dateTimeCultureInfo, DateTimeStyles.None, out _ ))
{
timeDateFormat = monthDayFormat;
}
else if (DateTime.TryParseExact(androidLogCatMatch.Groups[1].Value, monthDayYearFormat, dateTimeCultureInfo, DateTimeStyles.None, out _ ))
{
timeDateFormat = monthDayYearFormat;
}
else
{
throw new Exception($"Invalid date/time format: {androidLogCatMatch.Groups[1].Value}");
}
}
// First, we check if the line is a new log entry if it matched Regex and by trying to parse its timestamp
if (androidLogCatMatch.Success &&
androidLogCatMatch.Groups.Count >= 7 &&
DateTime.TryParseExact(androidLogCatMatch.Groups[1].Value, "MM-dd HH:mm:ss.fff", dateTimeCultureInfo, DateTimeStyles.None, out DateTime parsedTime))
DateTime.TryParseExact(androidLogCatMatch.Groups[1].Value, timeDateFormat, dateTimeCultureInfo, DateTimeStyles.None, out DateTime parsedTime))
{
var timeStamp = Timestamp.FromNanoseconds(parsedTime.Ticks * 100);
@ -115,12 +135,12 @@ namespace LinuxLogParser.AndroidLogcat
{
var messageSplit = logEntry.Message.Split();
var firstSingleQuoteIdx = logEntry.Message.IndexOf('\'');
var secondSingleQuoteIdx = logEntry.Message.IndexOf('\'', firstSingleQuoteIdx+1);
var name = logEntry.Message.Substring(firstSingleQuoteIdx+1, secondSingleQuoteIdx - firstSingleQuoteIdx - 1);
var secondSingleQuoteIdx = logEntry.Message.IndexOf('\'', firstSingleQuoteIdx + 1);
var name = logEntry.Message.Substring(firstSingleQuoteIdx + 1, secondSingleQuoteIdx - firstSingleQuoteIdx - 1);
// Command 'write /dev/cpuctl/cpu.rt_period_us 1000000' action=init (/system/etc/init/hw/init.rc:271) took 0ms and failed: ....
if (logEntry.Message.Contains("0ms"))
{
durationLogEntry = new DurationLogEntry(logEntry, logEntry.Timestamp - oneNanoSecondTimestampDelta, oneNanoSecondTimestampDelta, name);
durationLogEntry = LogEntryFromDurationNs(0, logEntry, name);
}
// Service 'boringssl_self_test32_vendor' (pid 18) exited with status 0 waiting took 0.022000 seconds
@ -128,17 +148,15 @@ namespace LinuxLogParser.AndroidLogcat
{
if (double.TryParse(messageSplit[^2], out double durationSeconds))
{
var duration = new TimestampDelta((long)(durationSeconds * SECONDS_TO_NS));
durationLogEntry = new DurationLogEntry(logEntry, logEntry.Timestamp - duration, duration, name);
durationLogEntry = LogEntryFromDurationS(durationSeconds, logEntry, name);
}
}
// Command 'mount_all /fstab.${ro.hardware}' action=fs (/vendor/etc/init/init.windows_x86_64.rc:14) took 1054ms and succeeded
else if(messageSplit[^4] == "took" && messageSplit[^1] == "succeeded")
else if (messageSplit[^4] == "took" && messageSplit[^1] == "succeeded")
{
if (int.TryParse(messageSplit[^3].Replace("ms", String.Empty), out int durationMs))
{
var duration = new TimestampDelta(durationMs * MS_TO_NS);
durationLogEntry = new DurationLogEntry(logEntry, logEntry.Timestamp - duration, duration, name);
durationLogEntry = LogEntryFromDurationMs(durationMs, logEntry, name);
}
}
// Service 'disable_lro' (pid 39) exited with status 0 oneshot service took 0.006000 seconds in background
@ -146,8 +164,7 @@ namespace LinuxLogParser.AndroidLogcat
{
if (double.TryParse(messageSplit[^4], out double durationSeconds))
{
var duration = new TimestampDelta((long)(durationSeconds * SECONDS_TO_NS));
durationLogEntry = new DurationLogEntry(logEntry, logEntry.Timestamp - duration, duration, name);
durationLogEntry = LogEntryFromDurationS(durationSeconds, logEntry, name);
}
}
// Command 'rm /data/user/0' action=post-fs-data (/system/etc/init/hw/init.rc:706) took 1ms and failed: unlink() failed: Is a directory
@ -159,8 +176,7 @@ namespace LinuxLogParser.AndroidLogcat
{
if (int.TryParse(afterTook.Replace("ms", String.Empty), out int durationMs))
{
var duration = new TimestampDelta(durationMs * MS_TO_NS);
durationLogEntry = new DurationLogEntry(logEntry, logEntry.Timestamp - duration, duration, name);
durationLogEntry = LogEntryFromDurationMs(durationMs, logEntry, name);
}
}
}
@ -172,14 +188,73 @@ namespace LinuxLogParser.AndroidLogcat
var name = messageSplit[0];
if (int.TryParse(messageSplit[^1].Replace("ms", String.Empty), out int durationMs))
{
if (durationMs == 0)
durationLogEntry = LogEntryFromDurationMs(durationMs, logEntry, name);
}
}
else if (logEntry.Tag == "SurfaceFlinger" && logEntry.Message.Contains("Boot is finished"))
{
var messageSplit = logEntry.Message.Split();
if (int.TryParse(messageSplit[^2].Replace("(", String.Empty), out int durationMs))
{
durationLogEntry = LogEntryFromDurationMs(durationMs, logEntry, "Boot is finished");
}
}
else if (logEntry.Tag == "Looper" && logEntry.Message.StartsWith("Slow"))
{
var messageSplit = logEntry.Message.Split();
if (messageSplit.Length >= 6 && int.TryParse(messageSplit[3].Replace("ms", String.Empty), out int durationMs))
{
durationLogEntry = LogEntryFromDurationMs(durationMs, logEntry, $"{messageSplit[4]} {messageSplit[5]}");
}
}
else if (logEntry.Tag == "OpenGLRenderer" && logEntry.Message.StartsWith("Davey!")) // Jank & Perf
{
var messageSplit = logEntry.Message.Split();
if (messageSplit.Length >= 2 && int.TryParse(messageSplit[1].Replace("duration=", String.Empty).Replace("ms;", String.Empty), out int durationMs))
{
durationLogEntry = LogEntryFromDurationMs(durationMs, logEntry, "OpenGLRenderer Jank Perf");
}
}
else if (logEntry.Tag == "Zygote" && logEntry.Message.EndsWith("ms."))
{
var messageSplit = logEntry.Message.Split();
if (int.TryParse(messageSplit[^1].Replace("ms.", String.Empty), out int durationMs))
{
durationLogEntry = LogEntryFromDurationMs(durationMs, logEntry, logEntry.Tag);
}
}
// Android 12
else if (logEntry.Tag == "ServiceManager" && logEntry.Message.Contains("successful after waiting"))
{
var messageSplit = logEntry.Message.Split();
if (messageSplit.Length >= 3 && int.TryParse(messageSplit[^1].Replace("ms", String.Empty), out int durationMs))
{
durationLogEntry = LogEntryFromDurationMs(durationMs, logEntry, messageSplit[3].Replace("'", String.Empty));
}
}
else if (logEntry.Tag == "PackageManager" && logEntry.Message.StartsWith("Finished scanning"))
{
var messageSplit = logEntry.Message.Split();
if (messageSplit.Length >= 6 && int.TryParse(messageSplit[5], out int durationMs))
{
durationLogEntry = LogEntryFromDurationMs(durationMs, logEntry, messageSplit[2] + messageSplit[3]);
}
}
else if (logEntry.Tag == "dex2oat32" && logEntry.Message.StartsWith("dex2oat took"))
{
var messageSplit = logEntry.Message.Split();
if (messageSplit[2].EndsWith("ms"))
{
if (messageSplit.Length >= 3 && double.TryParse(messageSplit[2].Replace("ms", String.Empty), out double durationMs))
{
durationLogEntry = new DurationLogEntry(logEntry, logEntry.Timestamp - oneNanoSecondTimestampDelta, oneNanoSecondTimestampDelta, name);
durationLogEntry = LogEntryFromDurationMs(durationMs, logEntry, logEntry.Tag);
}
else
}
else
{
if (messageSplit.Length >= 3 && double.TryParse(messageSplit[2].Replace("s", String.Empty), out double durationS))
{
var duration = new TimestampDelta(durationMs * MS_TO_NS);
durationLogEntry = new DurationLogEntry(logEntry, logEntry.Timestamp - duration, duration, name);
durationLogEntry = LogEntryFromDurationS(durationS, logEntry, logEntry.Tag);
}
}
}
@ -245,5 +320,34 @@ namespace LinuxLogParser.AndroidLogcat
dataSourceInfo = new DataSourceInfo(0, offsetEndTimestamp.ToNanoseconds, DateTime.FromFileTimeUtc(fileStartTime.ToFileTime())); // Treat as current locale local time (default)
}
}
private DurationLogEntry LogEntryFromDurationS(double durationS, LogEntry logEntry, string name)
{
return LogEntryFromDurationNs((long) (durationS * SECONDS_TO_NS), logEntry, name);
}
private DurationLogEntry LogEntryFromDurationMs(int durationMs, LogEntry logEntry, string name)
{
return LogEntryFromDurationNs(durationMs * MS_TO_NS, logEntry, name);
}
private DurationLogEntry LogEntryFromDurationMs(double durationMs, LogEntry logEntry, string name)
{
return LogEntryFromDurationNs((long) (durationMs * MS_TO_NS), logEntry, name);
}
private DurationLogEntry LogEntryFromDurationNs(long durationNs, LogEntry logEntry, string name)
{
if (durationNs == 0)
{
return new DurationLogEntry(logEntry, logEntry.Timestamp - OneNanoSecondTimestampDelta, OneNanoSecondTimestampDelta, name);
}
else
{
var duration = new TimestampDelta(durationNs);
return new DurationLogEntry(logEntry, logEntry.Timestamp - duration, duration, name);
}
}
}
}

Просмотреть файл

@ -116,12 +116,16 @@ namespace LinuxLogParsersUnitTest
Assert.IsTrue(eventData.DurationLogEntries[1].Duration.ToMilliseconds == 53);
Assert.IsTrue((eventData.DurationLogEntries[1].EndTimestamp - eventData.DurationLogEntries[1].StartTimestamp).ToMilliseconds == 53);
Assert.IsTrue(eventData.DurationLogEntries.Count == 1011);
Assert.IsTrue(eventData.DurationLogEntries.Count == 1025);
// MakeWindowManagerServiceReady took to complete: 3ms
Assert.IsTrue(eventData.DurationLogEntries[443].Name == "MakeWindowManagerServiceReady");
Assert.IsTrue(eventData.DurationLogEntries[443].Duration.ToMilliseconds == 3);
Assert.IsTrue((eventData.DurationLogEntries[443].EndTimestamp - eventData.DurationLogEntries[443].StartTimestamp).ToMilliseconds == 3);
Assert.IsTrue(eventData.DurationLogEntries[455].Name == "MakeWindowManagerServiceReady");
Assert.IsTrue(eventData.DurationLogEntries[455].Duration.ToMilliseconds == 3);
Assert.IsTrue((eventData.DurationLogEntries[455].EndTimestamp - eventData.DurationLogEntries[455].StartTimestamp).ToMilliseconds == 3);
// Looper : Slow delivery took 2033ms main h=android.app.ActivityThread$H c=null m=156
Assert.IsTrue(eventData.DurationLogEntries[921].Name == "main h=android.app.ActivityThread$H");
Assert.IsTrue(eventData.DurationLogEntries[921].Duration.ToMilliseconds == 2033);
}
[TestMethod]

Просмотреть файл

@ -44,7 +44,7 @@ namespace AndroidLogcatMPTAddin.Tables
private static readonly ColumnConfiguration DurationColumn = new ColumnConfiguration(
new ColumnMetadata(new Guid("{3894B186-D168-4E46-B0B5-0091BD5807A1}"), "Duration", "Duration of the event"),
new UIHints { Width = 80, CellFormat = "ms" });
new UIHints { Width = 80, CellFormat = TimestampFormatter.FormatMillisecondsGrouped });
private static readonly ColumnConfiguration DurationColumnOrderedMax = new ColumnConfiguration(
new ColumnMetadata(new Guid("{3894B186-D168-4E46-B0B5-0091BD5807A1}"), "Duration", "Duration of the event"),
@ -141,14 +141,14 @@ namespace AndroidLogcatMPTAddin.Tables
MessageColumn,
DurationColumnOrderedMax,
TableConfiguration.GraphColumn,
StartTimestampColumnSorted,
StartTimestampColumn,
EndTimestampColumn
},
};
longestDurationConfig.AddColumnRole(ColumnRole.StartTime, StartTimestampColumnSorted);
longestDurationConfig.AddColumnRole(ColumnRole.EndTime, EndTimestampColumn);
longestDurationConfig.AddColumnRole(ColumnRole.Duration, DurationColumn);
longestDurationConfig.AddColumnRole(ColumnRole.Duration, DurationColumnOrderedMax);
tableBuilder.AddTableConfiguration(timeOrderConfig)
.AddTableConfiguration(longestDurationConfig)

Просмотреть файл

@ -88,6 +88,7 @@ namespace AndroidLogcatMPTAddin.Tables
PriorityColumn,
TagColumn,
MessageColumn,
TableConfiguration.RightFreezeColumn,
TableConfiguration.GraphColumn,
TimestampColumn
},

Просмотреть файл

@ -18,7 +18,14 @@ Logs:
- Logs.Verbose=y
- [AndroidLogcat](https://developer.android.com/studio/command-line/logcat)
- Default log format should be supported
- Durations are supported/parsed if the logging includes init timing or *Timing logs such as SystemServerTiming
- Basic durations are supported/parsed on production builds / logs.
- E.g. "SurfaceFlinger: Boot is finished (9550 ms)"
- Enhanced durations are supported/parsed, if the logging includes init timing or *Timing logs such as SystemServerTiming. Perf durations are available in-development with [userdebug builds](https://source.android.com/setup/develop/new-device#userdebug-guidelines).
- E.g "SystemServerTimingAsync: InitThreadPoolExec:prepareAppData took to complete: 149ms"
- Logcat can optionally log the year but defaults to not. If the year is not provided the year is assumed to be the current year on the analysis machine.
- If this is incorrect, for example trace was captured in 2021, but analyzed in 2022 then the year will be interpreted incorrectly.
- This applies only if you need correct absolute timestamps, as relative timestamps will still be good.
- Manual workaround: In the logcat log search/replace to add year. E.g. "12-21" -> "12-21-2021"
- Logs are logged in local time zone and default assumed to be loaded in same time zone as captured
- To provide a hint on the timezone if in a different zone
- Place a "utcoffset.txt" file in the same folder as the trace. Place the UTC+Offset in the file as a double in hours.