| <!DOCTYPE html> |
| <!-- |
| Copyright 2017 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/base/math/range.html"> |
| <link rel="import" href="/tracing/metrics/metric_registry.html"> |
| <link rel="import" href="/tracing/value/histogram.html"> |
| |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.metrics.sh', function() { |
| const MAX_INPUT_EVENT_TO_STARTUP_DELAY_IN_MS = 2000; |
| // Post-startup activity draw delay. |
| const MIN_DRAW_DELAY_IN_MS = 80; |
| const MAX_DRAW_DELAY_IN_MS = 2000; |
| |
| function findProcess(processName, model) { |
| for (const pid in model.processes) { |
| const process = model.processes[pid]; |
| if (process.name === processName) { |
| return process; |
| } |
| } |
| return undefined; |
| } |
| |
| function findThreads(process, threadPrefix) { |
| if (process === undefined) return undefined; |
| const threads = []; |
| for (const tid in process.threads) { |
| const thread = process.threads[tid]; |
| if (thread.name.startsWith(threadPrefix)) { |
| threads.push(thread); |
| } |
| } |
| return threads; |
| } |
| |
| function findUIThread(process) { |
| if (process === undefined) return undefined; |
| const threads = findThreads(process, 'UI Thread'); |
| if (threads !== undefined && threads.length === 1) { |
| return threads[0]; |
| } |
| return process.threads[process.pid]; |
| } |
| |
| // Returns slices with actual app's process startup, excluding other delays. |
| function findLaunchSlices(model) { |
| const launches = []; |
| const binders = findThreads(findProcess('system_server', model), 'Binder'); |
| for (const binderId in binders) { |
| const binder = binders[binderId]; |
| for (const sliceId in binder.asyncSliceGroup.slices) { |
| const slice = binder.asyncSliceGroup.slices[sliceId]; |
| if (slice.title.startsWith('launching:')) { |
| launches.push(slice); |
| } |
| } |
| } |
| return launches; |
| } |
| |
| // Try to find draw event when activity just shown. |
| function findDrawSlice(appName, startNotBefore, model) { |
| let drawSlice = undefined; |
| const thread = findUIThread(findProcess(appName, model)); |
| if (thread === undefined) return undefined; |
| |
| for (const sliceId in thread.sliceGroup.slices) { |
| const slice = thread.sliceGroup.slices[sliceId]; |
| if (slice.start < startNotBefore + MIN_DRAW_DELAY_IN_MS || |
| slice.start > startNotBefore + MAX_DRAW_DELAY_IN_MS) continue; |
| if (slice.title !== 'draw') continue; |
| // TODO(kraynov): Add reportFullyDrawn() support. |
| if (drawSlice === undefined || slice.start < drawSlice.start) { |
| drawSlice = slice; |
| } |
| } |
| return drawSlice; |
| } |
| |
| // Try to find input event before a process starts. |
| function findInputEventSlice(endNotAfter, model) { |
| const endNotBefore = endNotAfter - MAX_INPUT_EVENT_TO_STARTUP_DELAY_IN_MS; |
| let inputSlice = undefined; |
| const systemUi = findUIThread(findProcess('com.android.systemui', model)); |
| if (systemUi === undefined) return undefined; |
| |
| for (const sliceId in systemUi.asyncSliceGroup.slices) { |
| const slice = systemUi.asyncSliceGroup.slices[sliceId]; |
| if (slice.end > endNotAfter || slice.end < endNotBefore) continue; |
| if (slice.title !== 'deliverInputEvent') continue; |
| if (inputSlice === undefined || slice.end > inputSlice.end) { |
| inputSlice = slice; |
| } |
| } |
| return inputSlice; |
| } |
| |
| function computeStartupTimeInMs(appName, launchSlice, model) { |
| let startupStart = launchSlice.start; |
| let startupEnd = launchSlice.end; |
| const drawSlice = findDrawSlice(appName, launchSlice.end, model); |
| if (drawSlice !== undefined) { |
| startupEnd = drawSlice.end; |
| } |
| const inputSlice = findInputEventSlice(launchSlice.start, model); |
| if (inputSlice !== undefined) { |
| startupStart = inputSlice.start; |
| } |
| return startupEnd - startupStart; |
| } |
| |
| // App startup time metric. |
| function measureStartup(histograms, model) { |
| const launches = findLaunchSlices(model); |
| for (const sliceId in launches) { |
| const launchSlice = launches[sliceId]; |
| const appName = launchSlice.title.split(': ')[1]; |
| const startupMs = computeStartupTimeInMs(appName, launchSlice, model); |
| histograms.createHistogram(`android:systrace:startup:${appName}`, |
| tr.b.Unit.byName.timeDurationInMs_smallerIsBetter, startupMs); |
| } |
| } |
| |
| // Metric which measures time spent by process threads in each thread state. |
| // The value of metric is a time percentage relative to the length of selected |
| // range of interest. |
| function measureThreadStates(histograms, model, rangeOfInterest) { |
| for (const pid in model.processes) { |
| const process = model.processes[pid]; |
| if (process.name === undefined) continue; |
| |
| let hasSlices = false; |
| let timeRunning = 0; |
| let timeRunnable = 0; |
| let timeSleeping = 0; |
| let timeUninterruptible = 0; |
| let timeBlockIO = 0; |
| let timeUnknown = 0; |
| |
| for (const tid in process.threads) { |
| const thread = process.threads[tid]; |
| if (thread.timeSlices === undefined) continue; |
| |
| for (const sliceId in thread.timeSlices) { |
| const slice = thread.timeSlices[sliceId]; |
| const sliceRange = |
| tr.b.math.Range.fromExplicitRange(slice.start, slice.end); |
| const intersection = rangeOfInterest.findIntersection(sliceRange); |
| const duration = intersection.duration; |
| if (duration === 0) continue; |
| hasSlices = true; |
| |
| if (slice.title === 'Running') { |
| timeRunning += duration; |
| } else if (slice.title === 'Runnable') { |
| timeRunnable += duration; |
| } else if (slice.title === 'Sleeping') { |
| timeSleeping += duration; |
| } else if (slice.title.startsWith('Uninterruptible')) { |
| timeUninterruptible += duration; |
| if (slice.title.includes('Block I/O')) timeBlockIO += duration; |
| } else { |
| timeUnknown += duration; |
| } |
| } |
| } |
| |
| if (hasSlices) { |
| // For sake of simplicity we don't count wall time for each |
| // thread/process and just calculate relative values against selected |
| // range of interest. |
| const wall = rangeOfInterest.max - rangeOfInterest.min; |
| histograms.createHistogram( |
| `android:systrace:threadtime:${process.name}:running`, |
| tr.b.Unit.byName.normalizedPercentage, timeRunning / wall); |
| histograms.createHistogram( |
| `android:systrace:threadtime:${process.name}:runnable`, |
| tr.b.Unit.byName.normalizedPercentage, timeRunnable / wall); |
| histograms.createHistogram( |
| `android:systrace:threadtime:${process.name}:sleeping`, |
| tr.b.Unit.byName.normalizedPercentage, timeSleeping / wall); |
| histograms.createHistogram( |
| `android:systrace:threadtime:${process.name}:blockio`, |
| tr.b.Unit.byName.normalizedPercentage, timeBlockIO / wall); |
| histograms.createHistogram( |
| `android:systrace:threadtime:${process.name}:uninterruptible`, |
| tr.b.Unit.byName.normalizedPercentage, timeUninterruptible / wall); |
| |
| // In case of changing names in systrace and importer. |
| if (timeUnknown > 0) { |
| histograms.createHistogram( |
| `android:systrace:threadtime:${process.name}:unknown`, |
| tr.b.Unit.byName.normalizedPercentage, timeUnknown / wall); |
| } |
| } |
| } |
| } |
| |
| function androidSystraceMetric(histograms, model, options) { |
| let rangeOfInterest = model.bounds; |
| if (options !== undefined && options.rangeOfInterest !== undefined) { |
| rangeOfInterest = options.rangeOfInterest; |
| } |
| |
| measureStartup(histograms, model); |
| measureThreadStates(histograms, model, rangeOfInterest); |
| } |
| |
| tr.metrics.MetricRegistry.register(androidSystraceMetric, { |
| supportsRangeOfInterest: true |
| }); |
| |
| return { |
| androidSystraceMetric, |
| }; |
| }); |
| </script> |