blob: 9dac4bf42d16b8b72c38530738c27d39571b2c67 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.util.brillopad;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.brillopad.item.GenericLogcatItem;
import com.android.tradefed.util.brillopad.item.LogcatItem;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An {@link IParser} to handle logcat. The parser can handle the time and threadtime logcat
* formats.
* <p>
* Since the timestamps in the logcat do not have a year, the year can be set manually when the
* parser is created or through {@link #setYear(String)}. If a year is not set, the current year
* will be used.
* </p>
*/
public class LogcatParser implements IParser {
/**
* Match a single line of `logcat -v threadtime`, such as:
* 05-26 11:02:36.886 5689 5689 D AndroidRuntime: CheckJNI is OFF
*/
private static final Pattern THREADTIME_LINE = Pattern.compile(
"^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" + /* timestamp [1] */
"(\\d+)\\s+(\\d+)\\s+([A-Z])\\s+" + /* pid/tid and log level [2-4] */
"(.+?)\\s*: (.*)$" /* tag and message [5-6]*/);
/**
* Match a single line of `logcat -v time`, such as:
* 06-04 02:32:14.002 D/dalvikvm( 236): GC_CONCURRENT freed 580K, 51% free [...]
*/
private static final Pattern TIME_LINE = Pattern.compile(
"^(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3})\\s+" + /* timestamp [1] */
"(\\w)/(.+?)\\(\\s*(\\d+)\\): (.*)$"); /* level, tag, pid, msg [2-5] */
/**
* Class for storing logcat meta data for a particular grouped list of lines.
*/
private class LogcatData {
public Integer mPid = null;
public Integer mTid = null;
public Date mTime = null;
public String mLevel = null;
public String mTag = null;
public String mLastPreamble = null;
public String mProcPreamble = null;
public List<String> mLines = new LinkedList<String>();
public LogcatData(Integer pid, Integer tid, Date time, String level, String tag,
String lastPreamble, String procPreamble) {
mPid = pid;
mTid = tid;
mTime = time;
mLevel = level;
mTag = tag;
mLastPreamble = lastPreamble;
mProcPreamble = procPreamble;
}
}
private static final int MAX_BUFF_SIZE = 500;
private static final int MAX_LAST_PREAMBLE_SIZE = 15;
private static final int MAX_PROC_PREAMBLE_SIZE = 15;
private LinkedList<String> mRingBuffer = new LinkedList<String>();
private String mYear = null;
LogcatItem mLogcat = new LogcatItem();
Map<String, LogcatData> mDataMap = new HashMap<String, LogcatData>();
List<LogcatData> mDataList = new LinkedList<LogcatData>();
private Date mStartTime = null;
private Date mStopTime = null;
/**
* Constructor for {@link LogcatParser}.
*/
public LogcatParser() {
}
/**
* Constructor for {@link LogcatParser}.
*
* @param year The year as a string.
*/
public LogcatParser(String year) {
setYear(year);
}
/**
* Sets the year for {@link LogcatParser}.
*
* @param year The year as a string.
*/
public void setYear(String year) {
mYear = year;
}
/**
* Parse a logcat from a {@link BufferedReader} into an {@link LogcatItem} object.
*
* @param input a {@link BufferedReader}.
* @return The {@link LogcatItem}.
* @see #parse(List)
*/
public LogcatItem parse(BufferedReader input) throws IOException {
String line;
while ((line = input.readLine()) != null) {
parseLine(line);
}
commit();
return mLogcat;
}
/**
* Parse a logcat from a {@link InputStreamSource} into an {@link LogcatItem} object.
*
* @param input a {@link InputStreamSource}.
* @return The {@link LogcatItem}.
* @see #parse(List)
*/
public LogcatItem parse(InputStreamSource input) throws IOException {
InputStream stream = input.createInputStream();
return parse(new BufferedReader(new InputStreamReader(stream)));
}
/**
* {@inheritDoc}
*
* @return The {@link LogcatItem}.
*/
public LogcatItem parse(List<String> lines) {
for (String line : lines) {
parseLine(line);
}
commit();
return mLogcat;
}
/**
* Parse a line of input.
*
* @param line The line to parse
*/
private void parseLine(String line) {
Integer pid = null;
Integer tid = null;
Date time = null;
String level = null;
String tag = null;
String msg = null;
Matcher m = THREADTIME_LINE.matcher(line);
Matcher tm = TIME_LINE.matcher(line);
if (m.matches()) {
time = parseTime(m.group(1));
pid = Integer.parseInt(m.group(2));
tid = Integer.parseInt(m.group(3));
level = m.group(4);
tag = m.group(5);
msg = m.group(6);
} else if (tm.matches()) {
time = parseTime(tm.group(1));
level = tm.group(2);
tag = tm.group(3);
pid = Integer.parseInt(tm.group(4));
msg = tm.group(5);
} else {
CLog.w("Failed to parse line '%s'", line);
return;
}
if (mStartTime == null) {
mStartTime = time;
}
mStopTime = time;
// ANRs are split when START matches a line. The newest entry is kept in the dataMap
// for quick lookup while all entries are added to the list.
if ("E".equals(level) && "ActivityManager".equals(tag)) {
String key = encodeLine(pid, tid, level, tag);
LogcatData data;
if (!mDataMap.containsKey(key) || AnrParser.START.matcher(msg).matches()) {
data = new LogcatData(pid, tid, time, level, tag, getLastPreamble(),
getProcPreamble(pid));
mDataMap.put(key, data);
mDataList.add(data);
} else {
data = mDataMap.get(key);
}
data.mLines.add(msg);
}
// PID and TID are enough to separate Java and native crashes.
if (("E".equals(level) && "AndroidRuntime".equals(tag)) ||
("I".equals(level) && "DEBUG".equals(tag))) {
String key = encodeLine(pid, tid, level, tag);
LogcatData data;
if (!mDataMap.containsKey(key)) {
data = new LogcatData(pid, tid, time, level, tag, getLastPreamble(),
getProcPreamble(pid));
mDataMap.put(key, data);
mDataList.add(data);
} else {
data = mDataMap.get(key);
}
data.mLines.add(msg);
}
// After parsing the line, add it the the buffer for the preambles.
mRingBuffer.add(line);
if (mRingBuffer.size() > MAX_BUFF_SIZE) {
mRingBuffer.removeFirst();
}
}
/**
* Signal that the input has finished.
*/
private void commit() {
for (LogcatData data : mDataList) {
GenericLogcatItem item = null;
if ("E".equals(data.mLevel) && "ActivityManager".equals(data.mTag)) {
CLog.v("Parsing ANR: %s", data.mLines);
item = new AnrParser().parse(data.mLines);
} else if ("E".equals(data.mLevel) && "AndroidRuntime".equals(data.mTag)) {
CLog.v("Parsing Java crash: %s", data.mLines);
item = new JavaCrashParser().parse(data.mLines);
} else if ("I".equals(data.mLevel) && "DEBUG".equals(data.mTag)) {
CLog.v("Parsing native crash: %s", data.mLines);
item = new NativeCrashParser().parse(data.mLines);
}
if (item != null) {
item.setEventTime(data.mTime);
item.setPid(data.mPid);
item.setTid(data.mTid);
item.setLastPreamble(data.mLastPreamble);
item.setProcessPreamble(data.mProcPreamble);
mLogcat.addEvent(item);
}
}
mLogcat.setStartTime(mStartTime);
mLogcat.setStopTime(mStopTime);
}
/**
* Create an identifier that "should" be unique for a given logcat. In practice, we do use it as
* a unique identifier.
*/
private static String encodeLine(Integer pid, Integer tid, String level, String tag) {
if (tid == null) {
return String.format("%d|%s|%s", pid, level, tag);
}
return String.format("%d|%d|%s|%s", pid, tid, level, tag);
}
/**
* Parse the timestamp and return a {@link Date}. If year is not set, the current year will be
* used.
*
* @param timeStr The timestamp in the format {@code MM-dd HH:mm:ss.SSS}.
* @return The {@link Date}.
*/
private Date parseTime(String timeStr) {
// If year is null, just use the current year.
if (mYear == null) {
DateFormat yearFormatter = new SimpleDateFormat("yyyy");
mYear = yearFormatter.format(new Date());
}
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
try {
return formatter.parse(String.format("%s-%s", mYear, timeStr));
} catch (ParseException e) {
CLog.e("Could not parse time string %s", timeStr);
return null;
}
}
/**
* Get the last {@value #MAX_LAST_PREAMBLE_SIZE} lines of logcat.
*/
private String getLastPreamble() {
final int size = mRingBuffer.size();
List<String> preamble;
if (size > getLastPreambleSize()) {
preamble = mRingBuffer.subList(size - getLastPreambleSize(), size);
} else {
preamble = mRingBuffer;
}
return ArrayUtil.join("\n", preamble).trim();
}
/**
* Get the last {@value #MAX_PROC_PREAMBLE_SIZE} lines of logcat which match the given pid.
*/
private String getProcPreamble(int pid) {
LinkedList<String> preamble = new LinkedList<String>();
ListIterator<String> li = mRingBuffer.listIterator(mRingBuffer.size());
while (li.hasPrevious()) {
String line = li.previous();
Matcher m = THREADTIME_LINE.matcher(line);
Matcher tm = TIME_LINE.matcher(line);
if ((m.matches() && pid == Integer.parseInt(m.group(2))) ||
(tm.matches() && pid == Integer.parseInt(tm.group(4)))) {
preamble.addFirst(line);
}
if (preamble.size() == getProcPreambleSize()) {
return ArrayUtil.join("\n", preamble).trim();
}
}
return ArrayUtil.join("\n", preamble).trim();
}
/**
* Get the number of lines in the last preamble. Exposed for unit testing.
*/
int getLastPreambleSize() {
return MAX_LAST_PREAMBLE_SIZE;
}
/**
* Get the number of lines in the process preamble. Exposed for unit testing.
*/
int getProcPreambleSize() {
return MAX_PROC_PREAMBLE_SIZE;
}
}