DevTools: Speed up aggregated stats pie chart calculation for timeline.
Precalculate and cache running accumulated value for each category, then
use a binary search to find values for right and left bounds and subtract
them.
BUG=874116
Change-Id: I3195ce4c38d52d048d453c5f999fb7005f0982ff
Reviewed-on: https://chromium-review.googlesource.com/1175241
Commit-Queue: Alexei Filippov <alph@chromium.org>
Reviewed-by: Andrey Kosyakov <caseq@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#583150}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 592df38489a10c29b5223fea2b166316f0b0cf80
diff --git a/front_end/timeline/TimelineUIUtils.js b/front_end/timeline/TimelineUIUtils.js
index 5416ef9..9e7c364 100644
--- a/front_end/timeline/TimelineUIUtils.js
+++ b/front_end/timeline/TimelineUIUtils.js
@@ -1057,36 +1057,38 @@
static statsForTimeRange(events, startTime, endTime) {
if (!events.length)
return {'idle': endTime - startTime};
- const symbol = Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol;
- Timeline.TimelineUIUtils._buildRangeStatsCacheIfNeeded(events);
- const before = findCachedStatsAfterTime(startTime);
- const statsBefore =
- subtractStats(before.stats, Timeline.TimelineUIUtils._slowStatsForTimeRange(events, startTime, before.time));
-
- const after = findCachedStatsAfterTime(endTime);
- const statsAfter =
- subtractStats(after.stats, Timeline.TimelineUIUtils._slowStatsForTimeRange(events, endTime, after.time));
-
- const aggregatedStats = subtractStats(statsAfter, statsBefore);
+ buildRangeStatsCacheIfNeeded(events);
+ const aggregatedStats = subtractStats(aggregatedStatsAtTime(endTime), aggregatedStatsAtTime(startTime));
const aggregatedTotal = Object.values(aggregatedStats).reduce((a, b) => a + b, 0);
aggregatedStats['idle'] = Math.max(0, endTime - startTime - aggregatedTotal);
return aggregatedStats;
/**
- * @param {number} atTime
- * @return {!{time: number, stats: !Object<string, number>}}
+ * @param {number} time
+ * @return {!Object}
*/
- function findCachedStatsAfterTime(atTime) {
- let index = events.lowerBound(atTime, (time, event) => time - (event.endTime || event.startTime));
- while (index < events.length && !events[index][symbol])
- index++;
- if (index === events.length) {
- const lastEvent = events.peekLast();
- return {time: lastEvent.endTime || lastEvent.startTime, stats: events[symbol]};
+ function aggregatedStatsAtTime(time) {
+ const stats = {};
+ const cache = events[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol];
+ for (const category in cache) {
+ const categoryCache = cache[category];
+ const index = categoryCache.time.upperBound(time);
+ let value;
+ if (index === 0) {
+ value = 0;
+ } else if (index === categoryCache.time.length) {
+ value = categoryCache.value.peekLast();
+ } else {
+ const t0 = categoryCache.time[index - 1];
+ const t1 = categoryCache.time[index];
+ const v0 = categoryCache.value[index - 1];
+ const v1 = categoryCache.value[index];
+ value = v0 + (v1 - v0) * (time - t0) / (t1 - t0);
+ }
+ stats[category] = value;
}
- const event = events[index];
- return {time: event.endTime || event.startTime, stats: event[symbol]};
+ return stats;
}
/**
@@ -1100,82 +1102,84 @@
result[key] -= b[key];
return result;
}
- }
-
- /**
- * @param {!Array<!SDK.TracingModel.Event>} events
- * @param {number} startTime
- * @param {number} endTime
- */
- static _slowStatsForTimeRange(events, startTime, endTime) {
- /** @type {!Object<string, number>} */
- const stats = {};
- const ownTimes = [];
-
- TimelineModel.TimelineModel.forEachEvent(
- events, onStartEvent, onEndEvent, undefined, startTime, endTime, Timeline.TimelineUIUtils._filterForStats());
/**
- * @param {!SDK.TracingModel.Event} e
+ * @param {!Array<!SDK.TracingModel.Event>} events
*/
- function onStartEvent(e) {
- const duration = Math.min(e.endTime, endTime) - Math.max(e.startTime, startTime);
- if (ownTimes.length)
- ownTimes[ownTimes.length - 1] -= duration;
- ownTimes.push(duration);
+ function buildRangeStatsCacheIfNeeded(events) {
+ if (events[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol])
+ return;
+
+ // aggeregatedStats is a map by categories. For each category there's an array
+ // containing sorted time points which records accumulated value of the category.
+ const aggregatedStats = {};
+ const categoryStack = [];
+ let lastTime = 0;
+ TimelineModel.TimelineModel.forEachEvent(
+ events, onStartEvent, onEndEvent, undefined, undefined, undefined, filterForStats());
+
+ /**
+ * @return {function(!SDK.TracingModel.Event):boolean}
+ */
+ function filterForStats() {
+ const visibleEventsFilter = Timeline.TimelineUIUtils.visibleEventsFilter();
+ return event => visibleEventsFilter.accept(event) || SDK.TracingModel.isTopLevelEvent(event);
+ }
+
+ /**
+ * @param {string} category
+ * @param {number} time
+ */
+ function updateCategory(category, time) {
+ let statsArrays = aggregatedStats[category];
+ if (!statsArrays) {
+ statsArrays = {time: [], value: []};
+ aggregatedStats[category] = statsArrays;
+ }
+ if (statsArrays.time.length && statsArrays.time.peekLast() === time)
+ return;
+ const lastValue = statsArrays.value.length ? statsArrays.value.peekLast() : 0;
+ statsArrays.value.push(lastValue + time - lastTime);
+ statsArrays.time.push(time);
+ }
+
+ /**
+ * @param {?string} from
+ * @param {?string} to
+ * @param {number} time
+ */
+ function categoryChange(from, to, time) {
+ if (from)
+ updateCategory(from, time);
+ lastTime = time;
+ if (to)
+ updateCategory(to, time);
+ }
+
+ /**
+ * @param {!SDK.TracingModel.Event} e
+ */
+ function onStartEvent(e) {
+ const category = Timeline.TimelineUIUtils.eventStyle(e).category.name;
+ const parentCategory = categoryStack.length ? categoryStack.peekLast() : null;
+ if (category !== parentCategory)
+ categoryChange(parentCategory, category, e.startTime);
+ categoryStack.push(category);
+ }
+
+ /**
+ * @param {!SDK.TracingModel.Event} e
+ */
+ function onEndEvent(e) {
+ const category = categoryStack.pop();
+ const parentCategory = categoryStack.length ? categoryStack.peekLast() : null;
+ if (category !== parentCategory)
+ categoryChange(category, parentCategory, e.endTime);
+ }
+
+ const obj = /** @type {!Object} */ (events);
+ obj[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol] = aggregatedStats;
}
-
- /**
- * @param {!SDK.TracingModel.Event} e
- */
- function onEndEvent(e) {
- const category = Timeline.TimelineUIUtils.eventStyle(e).category.name;
- stats[category] = (stats[category] || 0) + ownTimes.pop();
- }
- return stats;
- }
-
- /**
- * @return {function(!SDK.TracingModel.Event):boolean}
- */
- static _filterForStats() {
- const visibleEventsFilter = Timeline.TimelineUIUtils.visibleEventsFilter();
- return event => visibleEventsFilter.accept(event) || SDK.TracingModel.isTopLevelEvent(event);
- }
-
- /**
- * @param {!Array<!SDK.TracingModel.Event>} events
- */
- static _buildRangeStatsCacheIfNeeded(events) {
- if (events[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol])
- return;
-
- const aggregatedStats = {};
- const ownTimes = [];
- TimelineModel.TimelineModel.forEachEvent(
- events, onStartEvent, onEndEvent, undefined, undefined, undefined, Timeline.TimelineUIUtils._filterForStats());
-
- /**
- * @param {!SDK.TracingModel.Event} e
- */
- function onStartEvent(e) {
- if (ownTimes.length)
- ownTimes[ownTimes.length - 1] -= e.duration;
- ownTimes.push(e.duration);
- }
-
- /**
- * @param {!SDK.TracingModel.Event} e
- */
- function onEndEvent(e) {
- const category = Timeline.TimelineUIUtils.eventStyle(e).category.name;
- aggregatedStats[category] = (aggregatedStats[category] || 0) + ownTimes.pop();
- if (!ownTimes.length)
- e[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol] = Object.assign({}, aggregatedStats);
- }
-
- const obj = /** @type {!Object} */ (events);
- obj[Timeline.TimelineUIUtils._categoryBreakdownCacheSymbol] = Object.assign({}, aggregatedStats);
}
/**