Chromium Code Reviews| Index: tracing/tracing/extras/importer/android/event_log_importer.html |
| diff --git a/tracing/tracing/extras/importer/android/event_log_importer.html b/tracing/tracing/extras/importer/android/event_log_importer.html |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..46df8042fcebfc4f15ea106b329cc18ad5beac53 |
| --- /dev/null |
| +++ b/tracing/tracing/extras/importer/android/event_log_importer.html |
| @@ -0,0 +1,293 @@ |
| +<!DOCTYPE html> |
| +<!-- |
| +Copyright (c) 2015 The Chromium Authors. All rights reserved. |
| +Use of this source code is governed by a BSD-style license that can be |
| +found in the LICENSE file. |
| +--> |
| + |
| +<link rel="import" href="/tracing/importer/importer.html"> |
| +<link rel="import" href="/tracing/importer/simple_line_reader.html"> |
| +<link rel="import" href="/tracing/model/activity.html"> |
| +<link rel="import" href="/tracing/model/model.html"> |
| + |
| + |
| +<script> |
| +/** |
| + * @fileoverview Imports android event log data into the trace model. |
| + * Android event log data contains information about activities that |
| + * are launched/paused, processes that are started, memory usage, etc. |
| + * |
| + * The current implementation only parses activity events, with the goal of |
| + * determining which Activity is running in the foreground for a process. |
| + * |
| + * This importer assumes the events arrive as a string. The unit tests provide |
| + * examples of the trace format. |
| + * |
|
dsinclair
2015/08/27 15:18:50
nit: remove blank line.
coenen
2015/09/01 11:55:49
Done.
|
| + */ |
| +'use strict'; |
| + |
| +tr.exportTo('tr.e.importer.android', function() { |
| + var Importer = tr.importer.Importer; |
| + |
| + var ACTIVITY_STATE = { |
| + NONE: 'none', |
| + CREATED: 'created', |
| + STARTED: 'started', |
| + RESUMED: 'resumed', |
| + PAUSED: 'paused', |
| + STOPPED: 'stopped', |
| + DESTROYED: 'destroyed' |
| + }; |
| + |
| + var activityMap = {}; |
| + |
| + /** |
| + * Imports android event log data (adb logcat -b events) |
| + * @constructor |
| + */ |
| + function EventLogImporter(model, events) { |
| + this.model_ = model; |
| + this.events_ = events; |
| + this.importPriority = 3; |
|
dsinclair
2015/08/27 15:18:50
nit: _ on importPriority
coenen
2015/09/01 11:55:49
I think this is used externally to determine the o
|
| + } |
| + |
| + // Generic format of event log entries. |
| + // Sample event log entry that this matches (split over 2 lines): |
| + // 08-11 13:12:31.405 880 2645 I am_focused_activity: |
| + // [0,com.google.android.googlequicksearchbox/com.google.android.launcher.GEL] |
|
Chris Craik
2015/08/27 17:09:05
"// @suppress longLineCheck" needed here?
coenen
2015/09/01 11:55:49
Done.
|
| + |
| + var eventLogActivityRE = new RegExp( |
| + '(\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d+)' + |
| + '\\s*(\\d+)\\s*(\\d+)\\s*([A-Z])\\s*' + |
|
dsinclair
2015/08/27 15:18:50
I think the s*'s on this line should be s+'s. It l
coenen
2015/09/01 11:55:49
Yeah agreed, done
|
| + '(am_\\w+)\s*:(.*)'); |
| + |
| + var amCreateRE = new RegExp('\s*\\[.*,.*,.*,(.*),.*,.*,.*,.*\\]'); |
|
dsinclair
2015/08/27 15:18:50
Having examples for each of these as was done on l
coenen
2015/09/01 11:55:49
Done.
|
| + var amFocusedRE = new RegExp('\s*\\[\\d+,(.*)\\]'); |
| + var amProcStartRE = new RegExp('\s*\\[\\d+,\\d+,\\d+,.*,activity,(.*)\\]'); |
| + var amOnResumeRE = new RegExp('\s*\\[\\d+,(.*)\\]'); |
| + var amOnPauseRE = new RegExp('\s*\\[\\d+,(.*)\\]'); |
| + var amLaunchTimeRE = new RegExp('\s*\\[\\d+,\\d+,(.*),(\\d+),(\\d+)'); |
| + var amDestroyRE = new RegExp('\s*\\[\\d+,\\d+,\\d+,(.*)\\]'); |
| + |
| + /** |
| + * |
|
dsinclair
2015/08/27 15:18:50
nit: remove blank line.
coenen
2015/09/01 11:55:49
Done.
|
| + * @return {boolean} True when events is an android event log array. |
| + */ |
| + EventLogImporter.canImport = function(events) { |
| + if (!(typeof(events) === 'string' || events instanceof String)) |
| + return false; |
| + |
| + return eventLogActivityRE.test(events); |
| + }; |
| + |
| + EventLogImporter.prototype = { |
| + __proto__: Importer.prototype, |
| + |
| + get model() { |
| + return this.model_; |
| + }, |
| + |
| + /** |
| + * @return {string} the full activity name (including package) from |
| + * a component |
| + */ |
| + getFullActivityName: function(component) { |
| + var componentSplit = component.split('/'); |
| + if (componentSplit[1].startsWith('.')) |
| + return componentSplit[0] + componentSplit[1]; |
| + |
| + return componentSplit[1]; |
| + }, |
| + |
| + /** |
| + * @return {string} the process name of a component |
| + */ |
| + getProcName: function(component) { |
| + var componentSplit = component.split('/'); |
| + return componentSplit[0]; |
| + }, |
| + |
| + findOrCreateActivity: function(activityName) { |
| + if (activityName in activityMap) |
| + return activityMap[activityName]; |
| + var activity = {}; |
| + activity.state = ACTIVITY_STATE.NONE; |
| + activity.name = activityName; |
| + activityMap[activityName] = activity; |
|
dsinclair
2015/08/27 15:18:50
var activity = {
state: ACTIVITY_STATE.NONE,
n
coenen
2015/09/01 11:55:49
Done.
|
| + return activity; |
| + }, |
| + |
| + deleteActivity: function(activityName) { |
| + delete activityMap[activityName]; |
| + }, |
| + |
| + handleCreateActivity: function(ts, pid, activityName) { |
|
dsinclair
2015/08/27 15:18:50
pid is passed into a bunch of these, but not used,
coenen
2015/09/01 11:55:49
Done.
|
| + var activity = this.findOrCreateActivity(activityName); |
| + activity.state = ACTIVITY_STATE.CREATED; |
| + activity.createdTs = ts; |
| + }, |
| + |
| + handleFocusActivity: function(ts, pid, procName, activityName) { |
| + var activity = this.findOrCreateActivity(activityName); |
| + activity.lastFocusedTs = ts; |
| + }, |
| + |
| + handleProcStartForActivity: function(ts, pid, activityName) { |
| + var activity = this.findOrCreateActivity(activityName); |
| + activity.procStartTs = ts; |
| + }, |
| + |
| + handleOnResumeCalled: function(ts, pid, activityName) { |
| + var activity = this.findOrCreateActivity(activityName); |
| + activity.state = ACTIVITY_STATE.RESUMED; |
| + activity.lastResumeTs = ts; |
| + // On_resume_called shows the actual PID; use this |
|
dsinclair
2015/08/27 15:18:50
nit: on_remove_called (since that's the actual nam
coenen
2015/09/01 11:55:49
Done.
|
| + // to link the activity up with a process later |
| + activity.pid = pid; |
| + }, |
| + |
| + handleOnPauseCalled: function(ts, pid, activityName) { |
| + var activity = this.findOrCreateActivity(activityName); |
| + activity.state = ACTIVITY_STATE.PAUSED; |
| + activity.lastPauseTs = ts; |
| + // Create a new AndroidActivity representing the foreground state, |
| + // but only if the pause happened within the model bounds |
| + if (ts > this.model_.bounds.min && ts < this.model_.bounds.max) { |
| + this.addActivityToProcess(activity); |
| + } |
|
dsinclair
2015/08/27 15:18:50
nit: don't need {}'s for single line body.
coenen
2015/09/01 11:55:49
Done.
|
| + }, |
| + |
| + handleLaunchTime: function(ts, pid, activityName, launchTime) { |
| + var activity = this.findOrCreateActivity(activityName); |
| + activity.launchTime = launchTime; |
| + }, |
| + |
| + handleDestroyActivity: function(ts, pid, activityName) { |
| + this.deleteActivity(activityName); |
| + }, |
| + |
| + addActivityToProcess: function(activity) { |
| + if (activity.pid !== undefined) { |
|
dsinclair
2015/08/27 15:18:50
if (activity.pid === undefined)
return;
coenen
2015/09/01 11:55:49
Done.
|
| + var process = this.model_.getOrCreateProcess(activity.pid); |
| + // The range of the activity is the time from resume to time |
| + // of pause; limit the start time to the beginning of the model |
| + var range = tr.b.Range.fromExplicitRange( |
| + Math.max(this.model_.bounds.min, activity.lastResumeTs), |
| + activity.lastPauseTs); |
|
dsinclair
2015/08/27 15:18:50
Why does an activity only have one range? What hap
coenen
2015/09/01 11:55:49
With the current implementation, it'll add a separ
|
| + var newActivity = new tr.model.Activity(activity.name, |
| + 'Android Activity', range, |
| + {created: activity.createdTs, |
| + procstart: activity.procStartTs, |
| + lastfocus: activity.lastFocusedTs}); |
| + process.activities.push(newActivity); |
| + } |
| + }, |
| + |
| + parseAmLine_: function(line) { |
| + var match = eventLogActivityRE.exec(line); |
| + if (!match) |
| + return; |
| + |
| + // Possible activity life-cycles: |
| + // 1) Launch from scratch: |
| + // - am_create_activity |
| + // - am_focused_activity |
| + // - am_proc_start |
| + // - am_proc_bound |
| + // - am_restart_activity |
| + // - am_on_resume_called |
| + // 2) Re-open existing activity |
| + // - am_focused_activity |
| + // - am_on_resume_called |
| + |
| + // HACK: event log date format is "MM-DD" and doesn't contain the year; |
| + // to figure out the year, take the min bound of the model, convert |
| + // to real-time and use that as the year. |
| + // Working on getting Android event log to include the year. |
|
dsinclair
2015/08/27 15:18:50
Is there a bug for this we can link?
coenen
2015/09/01 11:55:49
There is a CL for it here; https://android-review.
|
| + var first_realtime_ts = this.model_.bounds.min - |
| + this.model_.realtime_to_monotonic_offset_ms; |
| + var year = new Date(first_realtime_ts).getFullYear(); |
| + var ts = match[1].substring(0, 5) + '-' + year + ' ' + |
| + match[1].substring(5, match[1].length); |
| + |
| + var monotonic_ts = Date.parse(ts) + |
| + this.model_.realtime_to_monotonic_offset_ms; |
| + |
| + var pid = match[2]; |
| + var action = match[5]; |
| + var data = match[6]; |
| + if (action === 'am_create_activity') { |
| + match = amCreateRE.exec(data); |
| + if (match && match.length >= 2) { |
| + this.handleCreateActivity(monotonic_ts, pid, |
| + this.getFullActivityName(match[1])); |
| + } |
| + } else if (action === 'am_focused_activity') { |
| + match = amFocusedRE.exec(data); |
| + if (match && match.length >= 2) { |
| + this.handleFocusActivity(monotonic_ts, pid, |
| + this.getProcName(match[1]), this.getFullActivityName(match[1])); |
| + } |
| + } else if (action === 'am_proc_start') { |
| + match = amProcStartRE.exec(data); |
| + if (match && match.length >= 2) { |
| + this.handleProcStartForActivity(monotonic_ts, pid, |
| + this.getFullActivityName(match[1])); |
| + } |
| + } else if (action === 'am_on_resume_called') { |
| + match = amOnResumeRE.exec(data); |
| + if (match && match.length >= 2) { |
| + this.handleOnResumeCalled(monotonic_ts, pid, match[1]); |
| + } |
|
dsinclair
2015/08/27 15:18:50
nit: {}'s
coenen
2015/09/01 11:55:49
Done.
|
| + } else if (action === 'am_on_paused_called') { |
| + match = amOnPauseRE.exec(data); |
| + if (match && match.length >= 2) { |
| + this.handleOnPauseCalled(monotonic_ts, pid, match[1]); |
|
dsinclair
2015/08/27 15:18:50
nit: {}'s
coenen
2015/09/01 11:55:49
Done.
|
| + } |
| + } else if (action === 'am_activity_launch_time') { |
| + match = amLaunchTimeRE.exec(data); |
| + this.handleLaunchTime(monotonic_ts, pid, |
| + this.getFullActivityName(match[1]), match[2]); |
| + } else if (action === 'am_destroy_activity') { |
| + match = amDestroyRE.exec(data); |
| + if (match && match.length == 2) { |
| + this.handleDestroyActivity(monotonic_ts, pid, |
| + this.getFullActivityName(match[1])); |
| + } |
| + } |
| + }, |
| + |
| + importEvents: function(isSecondaryImport) { |
| + // Check if we have a mapping from real-time to CLOCK_MONOTONIC |
| + if (isNaN(this.model_.realtime_to_monotonic_offset_ms)) { |
| + console.log('Need a trace_event_clock_sync to map realtime.'); |
|
dsinclair
2015/08/27 15:18:50
This should set an import error instead of logging
Chris Craik
2015/08/27 17:09:05
I assume old devices won't have the trace event sy
coenen
2015/09/01 11:55:49
Ok, changed to importWarning. I think we just refu
|
| + return; |
| + } |
| + // Since the event log typically spans a much larger timeframe |
| + // than the ftrace data, we want to calculate the bounds of the existing |
| + // model, and dump all event log data outside of those bounds |
| + this.model_.updateBounds(); |
| + |
| + var lines = this.events_.split('\n'); |
| + lines.forEach(this.parseAmLine_, this); |
| + |
| + // Iterate over all created activities that are not destroyed yet |
| + for (var activityName in activityMap) { |
| + var activity = activityMap[activityName]; |
| + // If we're still in the foreground, store the activity anyway |
| + if (activity.state == ACTIVITY_STATE.RESUMED) { |
| + // Set the pause timestamp to the end of the model bounds |
| + activity.lastPauseTs = this.model_.bounds.max; |
| + this.addActivityToProcess(activity); |
| + } |
| + } |
| + } |
| + }; |
| + |
| + tr.importer.Importer.register(EventLogImporter); |
|
dsinclair
2015/08/27 15:18:50
This was aliased above so just Importer should wor
coenen
2015/09/01 11:55:49
Done.
|
| + |
| + return { |
| + EventLogImporter: EventLogImporter |
| + }; |
| +}); |
| +</script> |