Chromium Code Reviews| Index: tracing/tracing/extras/chrome/cpu_time.html |
| diff --git a/tracing/tracing/extras/chrome/cpu_time.html b/tracing/tracing/extras/chrome/cpu_time.html |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..8cc6b00a814a5fe585f12402e86c7ed8e6dfc38e |
| --- /dev/null |
| +++ b/tracing/tracing/extras/chrome/cpu_time.html |
| @@ -0,0 +1,308 @@ |
| +<!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/multi_dimensional_view.html"> |
| +<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> |
| +<link rel="import" href="/tracing/model/helpers/chrome_renderer_helper.html"> |
| +<link rel="import" href="/tracing/extras/chrome/chrome_processes.html"> |
| + |
| +<script> |
| +'use strict'; |
| + |
| +tr.exportTo('tr.e.chrome.cpuTime', function() { |
| + /** |
| + * Returns the total cpu time consumed within |range| by |thread|. |
| + */ |
| + function getCpuTimeForThread(thread, range) { |
| + let totalCpuTime = 0; |
| + tr.b.math.iterateOverIntersectingIntervals( |
|
benjhayden
2017/04/07 18:05:17
High-level design question: associatedEvents?
Way
dproy
2017/04/07 18:35:10
Thank you so much for providing the context here!
|
| + thread.sliceGroup.topLevelSlices, |
| + slice => slice.start, slice => slice.end, |
| + range.min, range.max, |
| + slice => { |
| + if (slice.duration === 0) return; |
| + if (!slice.cpuDuration) return; |
| + |
| + const intersection = range.findIntersection(slice.range); |
| + const fractionOfSliceInsideRangeOfInterest = |
| + intersection.duration / slice.duration; |
| + |
| + // We assume that if a slice doesn't lie entirely inside the range of |
| + // interest, then the CPU time is evenly distributed inside of the |
| + // slice. |
| + totalCpuTime += |
| + slice.cpuDuration * fractionOfSliceInsideRangeOfInterest; |
| + }); |
| + |
| + return totalCpuTime; |
| + } |
| + |
| + /** |
| + * Returns two level map of rail stage to initiator type to set of associated |
| + * segments that intersects with |rangeOfInterest|. |
| + * |
| + * For each rail stage, we additionally have a value for the key |
| + * 'all_initiators' returns all the segments associated with that rail stage |
| + * across all initiator types. For completeness, there is an additional rail |
| + * stage 'all_stages' that has all the segments across all rail stages. |
| + * |
| + * The returned value is a |
| + * Map<string, Map<string, Set<tr.model.um.Segment>> object. |
| + * Example content of the map: |
| + * |
| + * { |
| + * 'Animation': { |
| + * 'CSS': {Segments for CSS Animation}, |
| + * 'Video': {Segments for Video Animation}, |
| + * ... |
| + * 'all_initiators': {All Animation segments} |
| + * }, |
| + * 'Response': { |
| + * 'Click': {Segments for Click Response}, |
| + * 'Scroll': {Segments for Scroll Response}, |
| + * ... |
| + * 'all_initiators': {All Response segments} |
| + * }, |
| + * ... |
| + * 'all_stages': { |
| + * 'all_initiators': {All segments intersecting with |rangeOfInterest|} |
| + * } |
| + * } |
| + * |
| + * @param {!Array<tr.model.um.Segment>} segments |
| + * @param {!Array<tr.b.math.Range>} rangeOfInterest |
| + */ |
| + function getStageToInitiatorToSegments(segments, rangeOfInterest) { |
| + const stageToInitiatorToSegments = new Map(); |
| + stageToInitiatorToSegments.set('all_stages', |
| + new Map([['all_initiators', new Set()]])); |
| + |
| + for (const segment of segments) { |
| + if (!rangeOfInterest.intersectsRangeInclusive(segment.range)) continue; |
| + stageToInitiatorToSegments.get( |
| + 'all_stages').get('all_initiators').add(segment); |
| + |
| + for (const exp of segment.expectations) { |
| + const stageTitle = exp.stageTitle; |
| + if (!stageToInitiatorToSegments.has(stageTitle)) { |
| + stageToInitiatorToSegments.set(stageTitle, |
| + new Map([['all_initiators', new Set()]])); |
| + } |
| + |
| + const initiatorToSegments = stageToInitiatorToSegments.get(stageTitle); |
| + initiatorToSegments.get('all_initiators').add(segment); |
| + |
| + const initiatorType = exp.initiatorType; |
| + if (initiatorType) { |
|
benjhayden
2017/04/07 18:05:17
I think all UEs should have initiatorType, right?
dproy
2017/04/07 18:35:10
Idle Expectation doesn't.
|
| + if (!initiatorToSegments.has(initiatorType)) { |
| + initiatorToSegments.set(initiatorType, new Set()); |
| + } |
| + initiatorToSegments.get(initiatorType).add(segment); |
| + } |
| + } |
| + } |
| + return stageToInitiatorToSegments; |
| + } |
| + |
| + /** |
| + * Returns a map of range in |ranges| to total cpu time used by |thread| |
| + * during that range. |
| + */ |
| + function computeCpuTimesForRanges_(ranges, thread) { |
| + const rangeToCpuTime = new Map(); |
| + for (const range of ranges) { |
| + rangeToCpuTime.set(range, getCpuTimeForThread(thread, range)); |
| + } |
| + return rangeToCpuTime; |
| + } |
| + |
| + /** |
| + * Returns a map of segment in |segments| to intersection of bounds of that |
| + * segment and |rangeOfInterest|. |
| + * |
| + * We create this map so that we have a unique intersected range for each |
| + * segment. This saves memory, and the unique range can be used as a key in |
| + * other maps. |
| + */ |
| + function getSegmentToBounds_(segments, rangeOfInterest) { |
| + const segmentToBounds = new Map(); |
| + for (const segment of segments) { |
| + segmentToBounds.set(segment, |
|
benjhayden
2017/04/07 18:05:17
Drop empty intersections?
dproy
2017/04/07 18:35:10
Yes - I missed that. Will update.
dproy
2017/04/07 18:53:48
Actually I changed my mind. |segments| is already
|
| + rangeOfInterest.findIntersection(segment.range)); |
| + } |
| + return segmentToBounds; |
| + } |
| + |
| + /** |
| + * Returns a two level map of rail stage to initiator type to array of ranges |
| + * associated with that stage and initiator type within |rangeOfInterest|. |
| + * |
| + * The returned data format is exactly similar to |
| + * |getStageToInitiatorToSegments|, except this function has array segment |
| + * bounds instead of array of segments in the leaf node. This function does |
| + * all the work of intersecting segment bounds with |rangeOfInterest| so that |
| + * the intersection logic does not have to sprinkled all over the rest of the |
| + * code. |
| + * |
| + */ |
| + function getStageToInitiatorToSegmentBounds(segments, rangeOfInterest) { |
| + const stageToInitiatorToSegments = |
| + getStageToInitiatorToSegments(segments, rangeOfInterest); |
| + const segmentToBounds = getSegmentToBounds_( |
| + stageToInitiatorToSegments.get('all_stages').get('all_initiators'), |
| + rangeOfInterest); |
| + |
| + const stageToInitiatorToRanges = new Map(); |
| + for (const [stage, initiatorToSegments] of stageToInitiatorToSegments) { |
| + const initiatorToRanges = new Map(); |
| + stageToInitiatorToRanges.set(stage, initiatorToRanges); |
| + for (const [initiator, segments] of initiatorToSegments) { |
| + const segmentBounds = |
| + [...segments].map( |
| + segment => segmentToBounds.get(segment)); |
| + initiatorToRanges.set(initiator, segmentBounds); |
| + } |
| + } |
| + |
| + return stageToInitiatorToRanges; |
| + } |
| + |
| + /** |
| + * Returns the root node of a MultiDimensionalView in TopDownTreeView for cpu |
| + * time. |
| + * |
| + * The returned tree view is three dimensional (processType, threadType, and |
| + * railStage + initiator). Rail stage and initiator are not separate |
| + * dimensions because they are not independent - there is no such thing as CSS |
| + * Response or Scroll Load. |
| + * |
| + * Each node in the tree view contains two values - cpuUsage and cpuTotal. |
| + * |
| + * When talking about multidimensional tree views, a useful abstration is |
| + * "path", which uniquely determines a node in the tree: A path is a 3 element |
| + * array, and each of these three elements is a possibly empty array of |
| + * strings. Here is an example path: |
| + * [ ["browser_process"], ["CrBrowserMain"], ["Animation", "CSS"] ] |
| + * Dimension 1 Dimension 2 Dimension 3 |
| + * |
| + * We can arrive at the node denoted by this path in many different ways |
| + * starting from the root node, so this path is not to be confused with the |
| + * graph theoretic notion of path. Here is one of the ways to reach the node |
| + * (we show the intermediate paths during the traversal inline): |
| + * |
| + * const node = treeRoot // [[], [], []] |
| + * // access children along first dimension |
| + * .children[0] |
| + * // [['browser_process'], [], []] |
| + * .get('browser_process') |
| + * // access children along third dimension |
| + * .children[2] |
| + * // [['browser_process'], [], ['Animation']] |
| + * .get('Animation') |
| + * // Access children along second dimension |
| + * .children[1] |
| + * // [['browser_process'], ['CrBrowserMain'], ['Animation']] |
| + * .get('CrBrowserMain') |
| + * // Go further down along third dimension |
| + * .children[2] |
| + * // [['browser_process'], ['CrBrowserMain'], ['Animation', 'CSS']] |
| + * .get('CSS') |
| + * |
| + * Now node.values contains the cpu time data for the browser main thread |
| + * during the CSS Animation stage. |
| + * node.values[0] is cpuUsage - cpu time over per unit of wall clock time |
| + * node.values[1] is cpuTotal - total miliseconds of used cpu time |
| + * |
| + * The path for the node that hold data for all threads of renderer process |
| + * during scroll response expectations is |
| + * [["renderer_process"], [], ["Response", "Scroll"]] |
| + * As we can see, we simply have an empty array for the second dimension. This |
| + * works similarly if we want to get data for all processes for a particular |
| + * thread. |
| + |
| + * However, if we want to access data for all rail stages and all initiator |
| + * types, we have to use the special rail stage 'all_stages', and initiator |
| + * type 'all_initiators'. For example, to get cpu data during all Response |
| + * stages for all processes and threads, we use the node at path |
| + * [[], [], ['Response', 'all_initiators']] |
| + * |
| + * To get cpu data for all rail stages for ChildIOThread, we use the path |
| + * [[], ['ChildIOThread'], ['all_stages', 'all_initiators']] |
| + * |
| + * This is because the tree view automatically aggregates cpu time |
| + * data along each dimension by summing values on the children nodes. For |
| + * aggregating rail stages and initiator types, summing is not the right thing |
| + * to do since |
| + * |
| + * 1. User Expectations can overlap (for example, one tab can go through a |
| + * Video Animation while another tab is concurrently going through a CSS |
| + * Animation - it's worth noting that user expectations are not scoped to a |
| + * tab.) |
| + * |
| + * 2. Different rail stages have different durations (for example, if we |
| + * have 200ms of Video Animation with 50% cpuUsage, and 500ms of CSS |
| + * Animation with 60% cpuUage, cpuUsage for all Animations is clearly not |
| + * 110%.) |
| + * |
| + * We therefore more manually do the appropriate aggregations and store the |
| + * data in 'all_stages' and 'all_initiators' nodes. |
| + * |
| + * This function adds all the data for the leaf nodes to a |
| + * |MultiDimensionalViewBuilder|. The builder then computes the rest of the |
| + * tree upon calling |buildTopDownTreeView|. |
| + */ |
| + function constructMultiDimensionalView(model, rangeOfInterest) { |
| + const mdvBuilder = new tr.b.MultiDimensionalViewBuilder( |
| + 3 /* dimensions (process, thread and rail stage / initiator) */, |
| + 2 /* valueCount (cpuUsage and cpuTotal) */); |
| + |
| + const stageToInitiatorToRanges = |
| + getStageToInitiatorToSegmentBounds( |
| + model.userModel.segments, rangeOfInterest); |
| + |
| + const allSegmentBoundsInRange = |
| + stageToInitiatorToRanges.get('all_stages').get('all_initiators'); |
| + |
| + for (const pid in model.processes) { |
| + const process = model.processes[pid]; |
| + const processType = |
| + tr.e.chrome.chrome_processes.canonicalizeProcessName(process.name); |
| + for (const tid in process.threads) { |
| + const thread = process.threads[tid]; |
| + const threadType = thread.type; |
| + |
| + // Cache cpuTime for each segment bound. |
| + const rangeToCpuTime = computeCpuTimesForRanges_( |
| + allSegmentBoundsInRange, thread); |
| + |
| + for (const [stage, initiatorToRanges] of stageToInitiatorToRanges) { |
| + for (const [initiator, ranges] of initiatorToRanges) { |
| + const cpuTime = tr.b.math.Statistics.sum(ranges, |
| + range => rangeToCpuTime.get(range)); |
| + const duration = tr.b.math.Statistics.sum(ranges, |
| + range => range.duration); |
| + const cpuTimePerSecond = cpuTime / duration; |
| + mdvBuilder.addPath( |
| + [[processType], [threadType], [stage, initiator]], |
| + [cpuTimePerSecond, cpuTime], |
| + tr.b.MultiDimensionalViewBuilder.ValueKind.TOTAL); |
| + } |
| + } |
| + } |
| + } |
| + |
| + return mdvBuilder.buildTopDownTreeView(); |
| + } |
| + |
| + return { |
| + getCpuTimeForThread, |
| + getStageToInitiatorToSegments, |
| + getStageToInitiatorToSegmentBounds, |
| + constructMultiDimensionalView, |
| + }; |
| +}); |
| +</script> |