Web Inspector: CPU Usage Timeline - Statistics and Sources sections
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 6 Mar 2019 20:38:43 +0000 (20:38 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 6 Mar 2019 20:38:43 +0000 (20:38 +0000)
https://bugs.webkit.org/show_bug.cgi?id=195202

Reviewed by Devin Rousso.

Source/WebInspectorUI:

* Localizations/en.lproj/localizedStrings.js:
New strings.

* UserInterface/Base/Utilities.js:
(Map.prototype.getOrInitialize):
Helper to get and if not found initialize with a value.

* UserInterface/Views/CPUTimelineView.css:
(.timeline-view.cpu > .content > .overview > .chart > .container.stats):
(.timeline-view.cpu > .content > .overview > .chart > .container.stats > table):
(.timeline-view.cpu > .content > .overview > .chart > .container.stats > table > tr > th):
(.timeline-view.cpu > .content > .overview > .chart > .container.stats > table > tr > td.number):
(.timeline-view.cpu > .content > .overview > .chart > .container.stats > table > tr > td.label):
(.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .show-more):
(.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .filter):
(.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .filter:hover):
(.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .active):
(.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .active + .active):
(@media (prefers-color-scheme: dark)):
Colors for the statistics sections.

* UserInterface/Views/CPUTimelineView.js:
(WI.CPUTimelineView):
(WI.CPUTimelineView.prototype.reset):
(WI.CPUTimelineView.prototype.clear):
(WI.CPUTimelineView.prototype._clearStatistics):
(WI.CPUTimelineView.prototype._clearSources):
Updates for additional sections.
Include a cache of the statisiticsData so we can relayout parts of the UI and
avoid an entire UI update.

(WI.CPUTimelineView.prototype.initialLayout):
(WI.CPUTimelineView.prototype._layoutBreakdownChart):
(WI.CPUTimelineView.prototype._layoutStatisticsAndSources):
(WI.CPUTimelineView.prototype._layoutStatisticsSection.createEllipsisElement):
(WI.CPUTimelineView.prototype._layoutStatisticsSection):
(WI.CPUTimelineView.prototype._layoutSourcesSection.firstNonNativeCallFrame):
(WI.CPUTimelineView.prototype._layoutSourcesSection.keyForSourceCodeLocation):
(WI.CPUTimelineView.prototype._layoutSourcesSection.labelForLocation):
(WI.CPUTimelineView.prototype._layoutSourcesSection.createEllipsisElement):
(WI.CPUTimelineView.prototype._layoutSourcesSection):
Extract layouts into helper methods to avoid an enormous layout method.

(WI.CPUTimelineView.prototype._computeSamplingData.incrementTypeCount):
(WI.CPUTimelineView.prototype._computeSamplingData):
Compute additional data when going through script events.

(WI.CPUTimelineView.prototype._resetSourcesFilters):
(WI.CPUTimelineView.prototype._addSourcesFilter):
(WI.CPUTimelineView.prototype._removeSourcesFilter):
(WI.CPUTimelineView.prototype._updateSourcesFilters):
Helpers for updating the source filters.

(WI.CPUTimelineView.prototype._createTableRow):
(WI.CPUTimelineView.prototype._insertTableRow):
Helpers for creating rows in the statistics / sources tables.

LayoutTests:

* inspector/unit-tests/map-utilities-expected.txt: Added.
* inspector/unit-tests/map-utilities.html: Added.
* inspector/unit-tests/set-utilities-expected.txt:
* inspector/unit-tests/set-utilities.html:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@242562 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/unit-tests/map-utilities-expected.txt [new file with mode: 0644]
LayoutTests/inspector/unit-tests/map-utilities.html [new file with mode: 0644]
LayoutTests/inspector/unit-tests/set-utilities-expected.txt
LayoutTests/inspector/unit-tests/set-utilities.html
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/Utilities.js
Source/WebInspectorUI/UserInterface/Models/TimelineRecord.js
Source/WebInspectorUI/UserInterface/Views/CPUTimelineView.css
Source/WebInspectorUI/UserInterface/Views/CPUTimelineView.js
Source/WebInspectorUI/UserInterface/Views/Main.css
Source/WebInspectorUI/UserInterface/Views/Variables.css

index ed78d20..159c726 100644 (file)
@@ -1,3 +1,15 @@
+2019-03-06  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: CPU Usage Timeline - Statistics and Sources sections
+        https://bugs.webkit.org/show_bug.cgi?id=195202
+
+        Reviewed by Devin Rousso.
+
+        * inspector/unit-tests/map-utilities-expected.txt: Added.
+        * inspector/unit-tests/map-utilities.html: Added.
+        * inspector/unit-tests/set-utilities-expected.txt:
+        * inspector/unit-tests/set-utilities.html:
+
 2019-03-06  Wenson Hsieh  <wenson_hsieh@apple.com>
 
         [iOS] Frequent 1 second IPC deadlocks when showing a paste callout
diff --git a/LayoutTests/inspector/unit-tests/map-utilities-expected.txt b/LayoutTests/inspector/unit-tests/map-utilities-expected.txt
new file mode 100644 (file)
index 0000000..111674d
--- /dev/null
@@ -0,0 +1,23 @@
+
+== Running test suite: Map
+-- Running test case: Map.fromObject
+PASS: Map from simple object should have size 2.
+PASS: Map from simple object should have keys: key1 and key2.
+PASS: Map from simple object should have values: value1 and value2.
+PASS: Map from empty object should be empty.
+
+-- Running test case: Map.prototype.take
+PASS: Map has `key`.
+PASS: Map has `key` => `value`.
+PASS: Map take `key` => `value`.
+PASS: Map does not have `key`.
+PASS: Map has `key` => `undefined`.
+PASS: Map take `doesNotExistKey` => `undefined`.
+
+-- Running test case: Map.prototype.getOrInitialize
+PASS: Map does not have `key`.
+PASS: Map should have initialized `key` with `value`.
+PASS: Map does have `key` => `value`.
+PASS: Map should get `key` with `value` without re-initializing.
+PASS: Map still has `key` => `value`.
+
diff --git a/LayoutTests/inspector/unit-tests/map-utilities.html b/LayoutTests/inspector/unit-tests/map-utilities.html
new file mode 100644 (file)
index 0000000..fcef6ac
--- /dev/null
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
+<script>
+function test()
+{
+    let suite = InspectorTest.createSyncSuite("Map");
+
+    suite.addTestCase({
+        name: "Map.fromObject",
+        test() {
+            const key1 = "key1";
+            const key2 = "key2";
+            const value1 = "value1";
+            const value2 = 200;
+            const object = {
+                [key1]: value1,
+                [key2]: value2,
+            };
+
+            let map = Map.fromObject(object);
+            InspectorTest.expectEqual(map.size, 2, "Map from simple object should have size 2.");
+            InspectorTest.expectThat(Array.shallowEqual(Array.from(map.keys()), [key1, key2]), "Map from simple object should have keys: key1 and key2.");
+            InspectorTest.expectThat(Array.shallowEqual(Array.from(map.values()), [value1, value2]), "Map from simple object should have values: value1 and value2.");
+
+            let emptyMap = Map.fromObject({});
+            InspectorTest.expectEqual(emptyMap.size, 0, "Map from empty object should be empty.");
+        }
+    });
+
+    suite.addTestCase({
+        name: "Map.prototype.take",
+        test() {
+            const key = "key";
+            const value = "value";
+
+            let map = new Map;
+            map.set(key, value);
+            InspectorTest.expectTrue(map.has(key), "Map has `key`.");
+            InspectorTest.expectEqual(map.get(key), value, "Map has `key` => `value`.");
+            InspectorTest.expectEqual(map.take(key), value, "Map take `key` => `value`.");
+            InspectorTest.expectFalse(map.has(key), "Map does not have `key`.");
+            InspectorTest.expectEqual(map.get(key), undefined, "Map has `key` => `undefined`.");
+            InspectorTest.expectEqual(map.take("doesNotExistKey"), undefined, "Map take `doesNotExistKey` => `undefined`.");
+        }
+    });
+
+    suite.addTestCase({
+        name: "Map.prototype.getOrInitialize",
+        test() {
+            const key = "key";
+            const value = "value";
+            const value2 = "value2";
+
+            let map = new Map;
+            InspectorTest.expectEqual(map.get(key), undefined, "Map does not have `key`.");
+            InspectorTest.expectEqual(map.getOrInitialize(key, value), value, "Map should have initialized `key` with `value`.");
+            InspectorTest.expectEqual(map.get(key), value, "Map does have `key` => `value`.");
+            InspectorTest.expectEqual(map.getOrInitialize(key, value2), value, "Map should get `key` with `value` without re-initializing.");
+            InspectorTest.expectEqual(map.get(key), value, "Map still has `key` => `value`.");
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onLoad="runTest()">
+</body>
+</html>
index b8da910..f844639 100644 (file)
@@ -1,5 +1,5 @@
 
-== Running test suite: SetUtilities
+== Running test suite: Set
 -- Running test case: Set.prototype.intersects
 PASS: an empty set should not intersect another empty set.
 PASS: a non-empty set should not intersect an empty set.
index db7140c..5033290 100644 (file)
@@ -5,7 +5,7 @@
 <script>
 function test()
 {
-    let suite = InspectorTest.createSyncSuite("SetUtilities");
+    let suite = InspectorTest.createSyncSuite("Set");
 
     suite.addTestCase({
         name: "Set.prototype.intersects",
@@ -28,8 +28,6 @@ function test()
             testFalse([1, "a", object1], [2, "b", object2], "a set should not intersect another set with different values.");
             testTrue([1, "a", object1], [1, 3, "a", "c", object1, object3], "a set should intersect another set with same and additional values.");
             testTrue([1, 2, "a", "b", object1, object2], [1, 3, "a", "c", object1, object3], "a set should intersect another set with same and different values.");
-
-            return true;
         }
     });
 
@@ -54,8 +52,6 @@ function test()
             testFalse([1, "a", object1], [2, "b", object2], "a set should not be a subset of another set with different values.");
             testTrue([1, "a", object1], [1, 3, "a", "c", object1, object3], "a set should be a subset of another set with same and additional values.");
             testFalse([1, 2, "a", "b", object1, object2], [1, 3, "a", "c", object1, object3], "a set should not be a subset of another set with same and different values.");
-
-            return true;
         }
     });
 
@@ -79,8 +75,6 @@ function test()
             testTrue([1, "a", object1], [object1, 1, "a"], "a set should be equal to another set with the same values in a different order.");
             testFalse([1, "a", object1], [2, "b", object2], "a set should not be a equal to another set with different values.");
             testFalse([1, 2, "a", "b", object1, object2], [1, 3, "a", "c", object1, object3], "a set should not be equal to another set with same and different values.");
-
-            return true;
         }
     });
 
@@ -121,8 +115,6 @@ function test()
                 bValues: [2, 3, 4],
                 expectedDifference: [1],
             });
-
-            return true;
         }
     });
 
index f4b8b38..f3acaf6 100644 (file)
@@ -1,5 +1,69 @@
 2019-03-06  Joseph Pecoraro  <pecoraro@apple.com>
 
+        Web Inspector: CPU Usage Timeline - Statistics and Sources sections
+        https://bugs.webkit.org/show_bug.cgi?id=195202
+
+        Reviewed by Devin Rousso.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        New strings.
+
+        * UserInterface/Base/Utilities.js:
+        (Map.prototype.getOrInitialize):
+        Helper to get and if not found initialize with a value.
+
+        * UserInterface/Views/CPUTimelineView.css:
+        (.timeline-view.cpu > .content > .overview > .chart > .container.stats):
+        (.timeline-view.cpu > .content > .overview > .chart > .container.stats > table):
+        (.timeline-view.cpu > .content > .overview > .chart > .container.stats > table > tr > th):
+        (.timeline-view.cpu > .content > .overview > .chart > .container.stats > table > tr > td.number):
+        (.timeline-view.cpu > .content > .overview > .chart > .container.stats > table > tr > td.label):
+        (.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .show-more):
+        (.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .filter):
+        (.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .filter:hover):
+        (.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .active):
+        (.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .active + .active):
+        (@media (prefers-color-scheme: dark)):
+        Colors for the statistics sections.
+
+        * UserInterface/Views/CPUTimelineView.js:
+        (WI.CPUTimelineView):
+        (WI.CPUTimelineView.prototype.reset):
+        (WI.CPUTimelineView.prototype.clear):
+        (WI.CPUTimelineView.prototype._clearStatistics):
+        (WI.CPUTimelineView.prototype._clearSources):
+        Updates for additional sections.
+        Include a cache of the statisiticsData so we can relayout parts of the UI and
+        avoid an entire UI update.
+
+        (WI.CPUTimelineView.prototype.initialLayout):
+        (WI.CPUTimelineView.prototype._layoutBreakdownChart):
+        (WI.CPUTimelineView.prototype._layoutStatisticsAndSources):
+        (WI.CPUTimelineView.prototype._layoutStatisticsSection.createEllipsisElement):
+        (WI.CPUTimelineView.prototype._layoutStatisticsSection):
+        (WI.CPUTimelineView.prototype._layoutSourcesSection.firstNonNativeCallFrame):
+        (WI.CPUTimelineView.prototype._layoutSourcesSection.keyForSourceCodeLocation):
+        (WI.CPUTimelineView.prototype._layoutSourcesSection.labelForLocation):
+        (WI.CPUTimelineView.prototype._layoutSourcesSection.createEllipsisElement):
+        (WI.CPUTimelineView.prototype._layoutSourcesSection):
+        Extract layouts into helper methods to avoid an enormous layout method.
+
+        (WI.CPUTimelineView.prototype._computeSamplingData.incrementTypeCount):
+        (WI.CPUTimelineView.prototype._computeSamplingData):
+        Compute additional data when going through script events.
+
+        (WI.CPUTimelineView.prototype._resetSourcesFilters):
+        (WI.CPUTimelineView.prototype._addSourcesFilter):
+        (WI.CPUTimelineView.prototype._removeSourcesFilter):
+        (WI.CPUTimelineView.prototype._updateSourcesFilters):
+        Helpers for updating the source filters.
+
+        (WI.CPUTimelineView.prototype._createTableRow):
+        (WI.CPUTimelineView.prototype._insertTableRow):
+        Helpers for creating rows in the statistics / sources tables.
+
+2019-03-06  Joseph Pecoraro  <pecoraro@apple.com>
+
         Web Inspector: Simplify chart <rect>s with x/y attributes instead of transform(x, y)
         https://bugs.webkit.org/show_bug.cgi?id=195352
 
index fd0e6e5..a4509d6 100644 (file)
@@ -122,6 +122,8 @@ localizedStrings["Anonymous Script %d"] = "Anonymous Script %d";
 localizedStrings["Anonymous Scripts"] = "Anonymous Scripts";
 localizedStrings["Anonymous StyleSheet %d"] = "Anonymous StyleSheet %d";
 localizedStrings["Application Cache"] = "Application Cache";
+/* Approximate count of events */
+localizedStrings["Approximate Number"] = "~%s";
 localizedStrings["Area"] = "Area";
 localizedStrings["Assertion"] = "Assertion";
 localizedStrings["Assertion Failed"] = "Assertion Failed";
@@ -427,8 +429,10 @@ localizedStrings["Evaluate JavaScript"] = "Evaluate JavaScript";
 localizedStrings["Event"] = "Event";
 localizedStrings["Event Breakpoint\u2026"] = "Event Breakpoint\u2026";
 localizedStrings["Event Dispatched"] = "Event Dispatched";
+localizedStrings["Event Handlers:"] = "Event Handlers:";
 localizedStrings["Event Listeners"] = "Event Listeners";
 localizedStrings["Events"] = "Events";
+localizedStrings["Events:"] = "Events:";
 localizedStrings["Example: \u201C%s\u201D"] = "Example: \u201C%s\u201D";
 localizedStrings["Exception with thrown value: %s"] = "Exception with thrown value: %s";
 localizedStrings["Exited Full-Screen Mode"] = "Exited Full-Screen Mode";
@@ -457,6 +461,7 @@ localizedStrings["File or Resource"] = "File or Resource";
 localizedStrings["Filename"] = "Filename";
 localizedStrings["Filter"] = "Filter";
 localizedStrings["Filter Full URL"] = "Filter Full URL";
+localizedStrings["Filter:"] = "Filter:";
 localizedStrings["Find Next (%s)"] = "Find Next (%s)";
 localizedStrings["Find Previous (%s)"] = "Find Previous (%s)";
 localizedStrings["Flows"] = "Flows";
@@ -686,6 +691,8 @@ localizedStrings["Not found"] = "Not found";
 localizedStrings["Object Graph"] = "Object Graph";
 localizedStrings["Object Store"] = "Object Store";
 localizedStrings["Observer Callback"] = "Observer Callback";
+localizedStrings["Observer Handlers:"] = "Observer Handlers:";
+localizedStrings["Observers:"] = "Observers:";
 localizedStrings["Off"] = "Off";
 localizedStrings["Once"] = "Once";
 localizedStrings["Online"] = "Online";
@@ -853,6 +860,7 @@ localizedStrings["Scope Chain"] = "Scope Chain";
 localizedStrings["Screen Shot %s-%s-%s at %s.%s.%s"] = "Screen Shot %s-%s-%s at %s.%s.%s";
 localizedStrings["Script"] = "Script";
 localizedStrings["Script Element %d"] = "Script Element %d";
+localizedStrings["Script Entries:"] = "Script Entries:";
 localizedStrings["Script Evaluated"] = "Script Evaluated";
 localizedStrings["Scripts"] = "Scripts";
 localizedStrings["Scroll Into View"] = "Scroll Into View";
@@ -956,6 +964,7 @@ localizedStrings["Start recording (%s)\nCreate new recording (%s)"] = "Start rec
 localizedStrings["Start recording canvas actions.\nShift-click to record a single frame."] = "Start recording canvas actions.\nShift-click to record a single frame.";
 localizedStrings["Start to Finish"] = "Start to Finish";
 localizedStrings["State"] = "State";
+localizedStrings["Statistics"] = "Statistics";
 localizedStrings["Status"] = "Status";
 localizedStrings["Step"] = "Step";
 localizedStrings["Step into (%s or %s)"] = "Step into (%s or %s)";
@@ -1033,6 +1042,7 @@ localizedStrings["Timer %d Removed"] = "Timer %d Removed";
 localizedStrings["Timer Fired"] = "Timer Fired";
 localizedStrings["Timer Installed"] = "Timer Installed";
 localizedStrings["Timer Removed"] = "Timer Removed";
+localizedStrings["Timers:"] = "Timers:";
 localizedStrings["Timestamp \u2014 %s"] = "Timestamp \u2014 %s";
 localizedStrings["Timing"] = "Timing";
 localizedStrings["To improve CPU utilization reduce or batch workloads when the page is not visible or during times when the page is not being interacted with."] = "To improve CPU utilization reduce or batch workloads when the page is not visible or during times when the page is not being interacted with.";
@@ -1063,6 +1073,7 @@ localizedStrings["Unable to show certificate for \u201C%s\u201D"] = "Unable to s
 localizedStrings["Uncaught Exceptions"] = "Uncaught Exceptions";
 localizedStrings["Undefined custom element"] = "Undefined custom element";
 localizedStrings["Unique"] = "Unique";
+localizedStrings["Unknown Location"] = "Unknown Location";
 localizedStrings["Unknown error"] = "Unknown error";
 localizedStrings["Unknown node"] = "Unknown node";
 localizedStrings["Unsupported property name"] = "Unsupported property name";
index dd78343..434b68a 100644 (file)
@@ -112,12 +112,27 @@ Object.defineProperty(Map.prototype, "take",
 {
     value(key)
     {
-        var deletedValue = this.get(key);
+        let deletedValue = this.get(key);
         this.delete(key);
         return deletedValue;
     }
 });
 
+Object.defineProperty(Map.prototype, "getOrInitialize",
+{
+    value(key, initialValue)
+    {
+        console.assert(initialValue !== undefined, "getOrInitialize should not be used with undefined.");
+
+        let value = this.get(key);
+        if (value)
+            return value;
+
+        this.set(key, initialValue);
+        return initialValue;
+    }
+});
+
 Object.defineProperty(Set.prototype, "equals",
 {
     value(other)
index bd4a995..2019a8d 100644 (file)
@@ -108,10 +108,9 @@ WI.TimelineRecord = class TimelineRecord extends WI.Object
             return null;
 
         // Return the first non-native code call frame as the initiator.
-        for (var i = 0; i < this._callFrames.length; ++i) {
-            if (this._callFrames[i].nativeCode)
-                continue;
-            return this._callFrames[i];
+        for (let frame of this._callFrames) {
+            if (!frame.nativeCode)
+                return frame;
         }
 
         return null;
index 2bbf483..b842b3d 100644 (file)
@@ -45,9 +45,9 @@ body .timeline-view.cpu {
     width: 15px;
     height: 15px;
     -webkit-margin-start: 7px;
-    color: white;
     font-size: 12px;
-    background-color: darkgray;
+    color: var(--gray-foreground-color);
+    background-color: var(--gray-background-color);
     border-radius: 50%;
 }
 
@@ -318,13 +318,80 @@ body[dir=rtl] .timeline-view.cpu :matches(.area-chart, .stacked-area-chart) .mar
     font-size: 14px;
 }
 
-@media (prefers-color-scheme: dark) {
-    .timeline-view.cpu > .content .subtitle > .info {
-        background-color: gray;
-    }
+.timeline-view.cpu > .content > .overview > .chart > .container.stats {
+    padding: 0 5px;
+    white-space: nowrap;
+    -webkit-user-select: text;
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table {
+    overflow: hidden;
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table > tr > th {
+    text-align: end;
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table > tr > td.number {
+    min-width: 25px;
+    padding: 0px 2px;
+    text-align: end;
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table > tr > td.label {
+    text-align: start;
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .show-more {
+    cursor: pointer;
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .unknown {
+    color: var(--link-text-color);
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .filter-clear {
+    display: inline-block;
+    width: 13px;
+    height: 13px;
+    font-size: 12px;
+    color: var(--gray-foreground-color);
+    background-color: var(--gray-background-color);
+    border-radius: 50%;
+    line-height: 12px;
+    text-align: center;
+    cursor: pointer;
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .filter {
+    padding: 0 6px 1px;
+    font-size: 10px;
+    background-color: hsl(0, 0%, 85%);
+    border: 1px solid transparent;
+    border-radius: 3px;
+    cursor: pointer;
+}
 
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table :matches(.filter, .filter-clear):hover {
+    opacity: 0.7;
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .filter.active {
+    color: var(--selected-foreground-color);
+    background-color: var(--selected-background-color);
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container.stats > table .filter.active + .filter.active {
+    -webkit-margin-start: 3px;
+}
+
+@media (prefers-color-scheme: dark) {
     .timeline-view.cpu .gauge-chart:not(.empty) > svg > polygon.needle {
         fill: hsla(0, 0%, var(--foreground-lightness), 0.85);
         stroke: hsla(0, 0%, var(--foreground-lightness), 0.85);
     }
+
+    .timeline-view.cpu > .content > .overview > .chart > .container.stats > table .filter {
+        background-color: hsl(0, 0%, 33%);
+    }
 }
index e3b1fd5..5aceff3 100644 (file)
@@ -35,6 +35,9 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
 
         this.element.classList.add("cpu");
 
+        this._statisticsData = null;
+        this._sectionLimit = CPUTimelineView.defaultSectionLimit;
+
         timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._cpuTimelineRecordAdded, this);
     }
 
@@ -63,6 +66,8 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
     static get mediumEnergyThreshold() { return 50; }
     static get highEnergyThreshold() { return 150; }
 
+    static get defaultSectionLimit() { return 5; }
+
     // Public
 
     shown()
@@ -83,6 +88,8 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
     {
         super.reset();
 
+        this._resetSourcesFilters();
+
         this.clear();
     }
 
@@ -99,6 +106,9 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         this._energyChart.needsLayout();
         this._clearEnergyImpactText();
 
+        this._clearStatistics();
+        this._clearSources();
+
         function clearUsageView(view) {
             view.clear();
 
@@ -115,6 +125,9 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         this._removeWorkerThreadViews();
 
         this._mainThreadWorkIndicatorView.clear();
+
+        this._statisticsData = null;
+        this._sectionLimit = CPUTimelineView.defaultSectionLimit;
     }
 
     // Protected
@@ -145,7 +158,8 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
             let chartSubtitleElement = chartElement.appendChild(document.createElement("div"));
             chartSubtitleElement.classList.add("subtitle");
             chartSubtitleElement.textContent = subtitle;
-            chartSubtitleElement.title = tooltip;
+            if (tooltip)
+                chartSubtitleElement.title = tooltip;
 
             let chartFlexContainerElement = chartElement.appendChild(document.createElement("div"));
             chartFlexContainerElement.classList.add("container");
@@ -189,8 +203,7 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         let dividerElement = overviewElement.appendChild(document.createElement("div"));
         dividerElement.classList.add("divider");
 
-        let energyTooltip = WI.UIString("Estimated energy impact.")
-        let energyContainerElement = createChartContainer(overviewElement, WI.UIString("Energy Impact"), energyTooltip);
+        let energyContainerElement = createChartContainer(overviewElement, WI.UIString("Energy Impact"), WI.UIString("Estimated energy impact."));
         energyContainerElement.classList.add("energy");
 
         let energyChartElement = energyContainerElement.parentElement;
@@ -288,7 +301,8 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         this._threadsDetailsElement.open = WI.settings.cpuTimelineThreadDetailsExpanded.value;
         this._threadsDetailsElement.addEventListener("toggle", (event) => {
             WI.settings.cpuTimelineThreadDetailsExpanded.value = this._threadsDetailsElement.open;
-            this.updateLayout();
+            if (this._threadsDetailsElement.open)
+                this.updateLayout(WI.CPUTimelineView.LayoutReason.Internal);
         });
 
         let threadsSubtitleElement = this._threadsDetailsElement.appendChild(document.createElement("summary"));
@@ -312,6 +326,78 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
 
         this._workerViews = [];
 
+        this._sourcesFilter = {
+            timer: new Set,
+            event: new Set,
+            observer: new Set,
+        };
+
+        let bottomOverviewElement = contentElement.appendChild(document.createElement("div"));
+        bottomOverviewElement.classList.add("overview");
+
+        let statisticsContainerElement = createChartContainer(bottomOverviewElement, WI.UIString("Statistics"));
+        statisticsContainerElement.classList.add("stats");
+
+        this._statisticsTable = statisticsContainerElement.appendChild(document.createElement("table"));
+        this._statisticsRows = [];
+
+        {
+            let {headerCell, numberCell} = this._createTableRow(this._statisticsTable);
+            headerCell.textContent = WI.UIString("Script Entries:");
+            this._scriptEntriesNumberElement = numberCell;
+        }
+
+        this._clearStatistics();
+
+        let bottomDividerElement = bottomOverviewElement.appendChild(document.createElement("div"));
+        bottomDividerElement.classList.add("divider");
+
+        let sourcesContainerElement = createChartContainer(bottomOverviewElement, WI.UIString("Sources"));
+        sourcesContainerElement.classList.add("stats");
+
+        this._sourcesTable = sourcesContainerElement.appendChild(document.createElement("table"));
+        this._sourcesRows = [];
+
+        {
+            let {row, headerCell, numberCell, labelCell} = this._createTableRow(this._sourcesTable);
+            headerCell.textContent = WI.UIString("Filter:");
+            this._sourcesFilterRow = row;
+            this._sourcesFilterRow.hidden = true;
+            this._sourcesFilterNumberElement = numberCell;
+            this._sourcesFilterLabelElement = labelCell;
+
+            let filterClearElement = numberCell.appendChild(document.createElement("span"));
+            filterClearElement.className = "filter-clear";
+            filterClearElement.textContent = multiplicationSign;
+            filterClearElement.addEventListener("click", (event) => {
+                this._resetSourcesFilters();
+                this._layoutStatisticsAndSources();
+            });
+        }
+        {
+            let {row, headerCell, numberCell, labelCell} = this._createTableRow(this._sourcesTable);
+            headerCell.textContent = WI.UIString("Timers:");
+            this._timerInstallationsRow = row;
+            this._timerInstallationsNumberElement = numberCell;
+            this._timerInstallationsLabelElement = labelCell;
+        }
+        {
+            let {row, headerCell, numberCell, labelCell} = this._createTableRow(this._sourcesTable);
+            headerCell.textContent = WI.UIString("Event Handlers:");
+            this._eventHandlersRow = row;
+            this._eventHandlersNumberElement = numberCell;
+            this._eventHandlersLabelElement = labelCell;
+        }
+        {
+            let {row, headerCell, numberCell, labelCell} = this._createTableRow(this._sourcesTable);
+            headerCell.textContent = WI.UIString("Observer Handlers:");
+            this._observerHandlersRow = row;
+            this._observerHandlersNumberElement = numberCell;
+            this._observerHandlersLabelElement = labelCell;
+        }
+
+        this._clearSources();
+
         this.element.addEventListener("mousemove", this._handleGraphMouseMove.bind(this));
     }
 
@@ -320,6 +406,9 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         if (this.layoutReason === WI.View.LayoutReason.Resize)
             return;
 
+        if (this.layoutReason !== WI.CPUTimelineView.LayoutReason.Internal)
+            this._sectionLimit = CPUTimelineView.defaultSectionLimit;
+
         // Always update timeline ruler.
         this._timelineRuler.zeroTime = this.zeroTime;
         this._timelineRuler.startTime = this.startTime;
@@ -345,37 +434,9 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
             return;
         }
 
-        let samplingData = this._computeSamplingData(graphStartTime, visibleEndTime);
-        let nonIdleSamplesCount = samplingData.samples.length - samplingData.samplesIdle;
-        if (!nonIdleSamplesCount) {
-            this._breakdownChart.clear();
-            this._breakdownChart.needsLayout();
-            this._clearBreakdownLegend();
-        } else {
-            let percentScript = samplingData.samplesScript / nonIdleSamplesCount;
-            let percentLayout = samplingData.samplesLayout / nonIdleSamplesCount;
-            let percentPaint = samplingData.samplesPaint / nonIdleSamplesCount;
-            let percentStyle = samplingData.samplesStyle / nonIdleSamplesCount;
-
-            this._breakdownLegendScriptElement.textContent = `${Number.percentageString(percentScript)} (${samplingData.samplesScript})`;
-            this._breakdownLegendLayoutElement.textContent = `${Number.percentageString(percentLayout)} (${samplingData.samplesLayout})`;
-            this._breakdownLegendPaintElement.textContent = `${Number.percentageString(percentPaint)} (${samplingData.samplesPaint})`;
-            this._breakdownLegendStyleElement.textContent = `${Number.percentageString(percentStyle)} (${samplingData.samplesStyle})`;
-
-            this._breakdownChart.values = [percentScript * 100, percentLayout * 100, percentPaint * 100, percentStyle * 100];
-            this._breakdownChart.needsLayout();
-
-            let centerElement = this._breakdownChart.centerElement;
-            let samplesElement = centerElement.firstChild;
-            if (!samplesElement) {
-                samplesElement = centerElement.appendChild(document.createElement("div"));
-                samplesElement.classList.add("samples");
-                samplesElement.title = WI.UIString("Time spent on the main thread");
-            }
-
-            let millisecondsStringNoDecimal = WI.UIString("%.0fms").format(nonIdleSamplesCount);
-            samplesElement.textContent = millisecondsStringNoDecimal;
-        }
+        this._statisticsData = this._computeStatisticsData(graphStartTime, visibleEndTime);
+        this._layoutBreakdownChart();
+        this._layoutStatisticsAndSources();
 
         let dataPoints = [];
         let workersDataMap = new Map;
@@ -637,13 +698,360 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
 
         let graphWidth = (graphEndTime - graphStartTime) / secondsPerPixel;
         let size = new WI.Size(graphWidth, CPUTimelineView.indicatorViewHeight);
-        this._mainThreadWorkIndicatorView.updateChart(samplingData.samples, size, visibleEndTime, xScaleIndicatorRange);
+        this._mainThreadWorkIndicatorView.updateChart(this._statisticsData.samples, size, visibleEndTime, xScaleIndicatorRange);
 
         this._layoutEnergyChart(average, visibleDuration);
     }
 
     // Private
 
+    _layoutBreakdownChart()
+    {
+        let {samples, samplesScript, samplesLayout, samplesPaint, samplesStyle, samplesIdle} = this._statisticsData;
+
+        let nonIdleSamplesCount = samples.length - samplesIdle;
+        if (!nonIdleSamplesCount) {
+            this._breakdownChart.clear();
+            this._breakdownChart.needsLayout();
+            this._clearBreakdownLegend();
+            return;
+        }
+
+        let percentScript = samplesScript / nonIdleSamplesCount;
+        let percentLayout = samplesLayout / nonIdleSamplesCount;
+        let percentPaint = samplesPaint / nonIdleSamplesCount;
+        let percentStyle = samplesStyle / nonIdleSamplesCount;
+
+        this._breakdownLegendScriptElement.textContent = `${Number.percentageString(percentScript)} (${samplesScript})`;
+        this._breakdownLegendLayoutElement.textContent = `${Number.percentageString(percentLayout)} (${samplesLayout})`;
+        this._breakdownLegendPaintElement.textContent = `${Number.percentageString(percentPaint)} (${samplesPaint})`;
+        this._breakdownLegendStyleElement.textContent = `${Number.percentageString(percentStyle)} (${samplesStyle})`;
+
+        this._breakdownChart.values = [percentScript * 100, percentLayout * 100, percentPaint * 100, percentStyle * 100];
+        this._breakdownChart.needsLayout();
+
+        let centerElement = this._breakdownChart.centerElement;
+        let samplesElement = centerElement.firstChild;
+        if (!samplesElement) {
+            samplesElement = centerElement.appendChild(document.createElement("div"));
+            samplesElement.classList.add("samples");
+            samplesElement.title = WI.UIString("Time spent on the main thread");
+        }
+
+        let millisecondsStringNoDecimal = WI.UIString("%.0fms").format(nonIdleSamplesCount);
+        samplesElement.textContent = millisecondsStringNoDecimal;
+    }
+
+    _layoutStatisticsAndSources()
+    {
+        this._layoutStatisticsSection();
+        this._layoutSourcesSection();
+    }
+
+    _layoutStatisticsSection()
+    {
+        let statistics = this._statisticsData;
+
+        this._clearStatistics();
+
+        this._scriptEntriesNumberElement.textContent = statistics.scriptEntries;
+
+        let createFilterElement = (type, name) => {
+            let span = document.createElement("span");
+            span.className = "filter";
+            span.textContent = name;
+            span.addEventListener("mouseup", (event) => {
+                if (span.classList.contains("active"))
+                    this._removeSourcesFilter(type, name);
+                else
+                    this._addSourcesFilter(type, name);
+
+                this._layoutStatisticsAndSources();
+            });
+
+            span.classList.toggle("active", this._sourcesFilter[type].has(name));
+
+            return span;
+        };
+
+        let expandAllSections = () => {
+            this._sectionLimit = Infinity;
+            this._layoutStatisticsAndSources();
+        };
+
+        function createEllipsisElement() {
+            let span = document.createElement("span");
+            span.className = "show-more";
+            span.role = "button";
+            span.textContent = ellipsis;
+            span.addEventListener("click", (event) => {
+                expandAllSections();
+            });
+            return span;
+        }
+
+        // Sort a Map of key => count values in descending order.
+        function sortMapByEntryCount(map) {
+            let entries = Array.from(map);
+            entries.sort((entryA, entryB) => entryB[1] - entryA[1]);
+            return new Map(entries);
+        }
+
+        if (statistics.timerTypes.size) {
+            let i = 0;
+            let sorted = sortMapByEntryCount(statistics.timerTypes);
+            for (let [timerType, count] of sorted) {
+                let headerValue = i === 0 ? WI.UIString("Timers:") : "";
+                let timerTypeElement = createFilterElement("timer", timerType);
+                this._insertTableRow(this._statisticsTable, this._statisticsRows, {headerValue, numberValue: count, labelValue: timerTypeElement});
+
+                if (++i === this._sectionLimit && sorted.size > this._sectionLimit) {
+                    this._insertTableRow(this._statisticsTable, this._statisticsRows, {labelValue: createEllipsisElement()});
+                    break;
+                }
+            }
+        }
+
+        if (statistics.eventTypes.size) {
+            let i = 0;
+            let sorted = sortMapByEntryCount(statistics.eventTypes);
+            for (let [eventType, count] of sorted) {
+                let headerValue = i === 0 ? WI.UIString("Events:") : "";
+                let eventTypeElement = createFilterElement("event", eventType);
+                this._insertTableRow(this._statisticsTable, this._statisticsRows, {headerValue, numberValue: count, labelValue: eventTypeElement});
+
+                if (++i === this._sectionLimit && sorted.size > this._sectionLimit) {
+                    this._insertTableRow(this._statisticsTable, this._statisticsRows, {labelValue: createEllipsisElement()});
+                    break;
+                }
+            }
+        }
+
+        if (statistics.observerTypes.size) {
+            let i = 0;
+            let sorted = sortMapByEntryCount(statistics.observerTypes);
+            for (let [observerType, count] of sorted) {
+                let headerValue = i === 0 ? WI.UIString("Observers:") : "";
+                let observerTypeElement = createFilterElement("observer", observerType);
+                this._insertTableRow(this._statisticsTable, this._statisticsRows, {headerValue, numberValue: count, labelValue: observerTypeElement});
+
+                if (++i === this._sectionLimit && sorted.size > this._sectionLimit) {
+                    this._insertTableRow(this._statisticsTable, this._statisticsRows, {labelValue: createEllipsisElement()});
+                    break;
+                }
+            }
+        }
+    }
+
+    _layoutSourcesSection()
+    {
+        let statistics = this._statisticsData;
+
+        this._clearSources();
+
+        const unknownLocationKey = "unknown";
+
+        function keyForSourceCodeLocation(sourceCodeLocation) {
+            if (!sourceCodeLocation)
+                return unknownLocationKey;
+
+            return sourceCodeLocation.sourceCode.url + ":" + sourceCodeLocation.lineNumber + ":" + sourceCodeLocation.columnNumber;
+        }
+
+        function labelForLocation(key, sourceCodeLocation, functionName) {
+            if (key === unknownLocationKey) {
+                let span = document.createElement("span");
+                span.className = "unknown";
+                span.textContent = WI.UIString("Unknown Location");
+                return span;
+            }
+
+            const options = {
+                nameStyle: WI.SourceCodeLocation.NameStyle.Short,
+                columnStyle: WI.SourceCodeLocation.ColumnStyle.Shown,
+                dontFloat: true,
+                ignoreNetworkTab: true,
+                ignoreSearchTab: true,
+            };
+            return WI.createSourceCodeLocationLink(sourceCodeLocation, options);
+        }
+
+        let timerFilters = this._sourcesFilter.timer;
+        let eventFilters = this._sourcesFilter.event;
+        let observerFilters = this._sourcesFilter.observer;
+        let hasFilters = (timerFilters.size || eventFilters.size || observerFilters.size);
+
+        let sectionLimit = this._sectionLimit;
+        if (isFinite(sectionLimit) && hasFilters)
+            sectionLimit = CPUTimelineView.defaultSectionLimit * 2;
+
+        let expandAllSections = () => {
+            this._sectionLimit = Infinity;
+            this._layoutStatisticsAndSources();
+        };
+
+        function createEllipsisElement() {
+            let span = document.createElement("span");
+            span.className = "show-more";
+            span.role = "button";
+            span.textContent = ellipsis;
+            span.addEventListener("click", (event) => {
+                expandAllSections();
+            });
+            return span;
+        }
+
+        let timerMap = new Map;
+        let eventHandlerMap = new Map;
+        let observerCallbackMap = new Map;
+        let seenTimers = new Set;
+
+        if (!hasFilters || timerFilters.size) {
+            // Aggregate timers on the location where the timers were installed.
+            // For repeating timers, this includes the total counts the interval fired in the selected time range.
+            for (let record of statistics.timerInstallationRecords) {
+                if (timerFilters.size) {
+                    if (record.eventType === WI.ScriptTimelineRecord.EventType.AnimationFrameRequested && !timerFilters.has("requestAnimationFrame"))
+                        continue;
+                    if (record.eventType === WI.ScriptTimelineRecord.EventType.TimerInstalled && !timerFilters.has("setTimeout"))
+                        continue;
+                }
+
+                let callFrame = record.initiatorCallFrame;
+                let sourceCodeLocation = callFrame ? callFrame.sourceCodeLocation : record.sourceCodeLocation;
+                let functionName = callFrame ? callFrame.functionName : "";
+                let key = keyForSourceCodeLocation(sourceCodeLocation);
+                let entry = timerMap.getOrInitialize(key, {sourceCodeLocation, functionName, count: 0, repeating: false});
+                if (record.details) {
+                    let timerIdentifier = record.details.timerId;
+                    let repeatingEntry = statistics.repeatingTimers.get(timerIdentifier);
+                    let count = repeatingEntry ? repeatingEntry.count : 1;
+                    entry.count += count;
+                    if (record.details.repeating)
+                        entry.repeating = true;
+                    seenTimers.add(timerIdentifier);
+                } else
+                    entry.count += 1;
+            }
+
+            // Aggregate repeating timers where we did not see the installation in the selected time range.
+            // This will use the source code location of where the timer fired, which is better than nothing.
+            if (!hasFilters || timerFilters.has("setTimeout")) {
+                for (let [timerId, repeatingEntry] of statistics.repeatingTimers) {
+                    if (seenTimers.has(timerId))
+                        continue;
+                    // FIXME: <https://webkit.org/b/195351> Web Inspector: CPU Usage Timeline - better resolution of installation source for repeated timers
+                    // We could have a map of all repeating timer installations in the whole recording
+                    // so that we can provide a function name for these repeating timers lacking an installation point.
+                    let sourceCodeLocation = repeatingEntry.record.sourceCodeLocation;
+                    let key = keyForSourceCodeLocation(sourceCodeLocation);
+                    let entry = timerMap.getOrInitialize(key, {sourceCodeLocation, count: 0, repeating: false});
+                    entry.count += repeatingEntry.count;
+                    entry.repeating = true;
+                }
+            }
+        }
+
+        if (!hasFilters || eventFilters.size) {
+            for (let record of statistics.eventHandlerRecords) {
+                if (eventFilters.size && !eventFilters.has(record.details))
+                    continue;
+                let sourceCodeLocation = record.sourceCodeLocation;
+                let key = keyForSourceCodeLocation(sourceCodeLocation);
+                let entry = eventHandlerMap.getOrInitialize(key, {sourceCodeLocation, count: 0});
+                entry.count += 1;
+            }
+        }
+
+        if (!hasFilters || observerFilters.size) {
+            for (let record of statistics.observerCallbackRecords) {
+                if (observerFilters.size && !observerFilters.has(record.details))
+                    continue;
+                let sourceCodeLocation = record.sourceCodeLocation;
+                let key = keyForSourceCodeLocation(record.sourceCodeLocation);
+                let entry = observerCallbackMap.getOrInitialize(key, {sourceCodeLocation, count: 0});
+                entry.count += 1;
+            }
+        }
+
+        const headerValue = "";
+
+        // Sort a Map of key => {count} objects in descending order.
+        function sortMapByEntryCountProperty(map) {
+            let entries = Array.from(map);
+            entries.sort((entryA, entryB) => entryB[1].count - entryA[1].count);
+            return new Map(entries);
+        }
+
+        if (timerMap.size) {
+            let i = 0;
+            let sorted = sortMapByEntryCountProperty(timerMap);
+            for (let [key, entry] of sorted) {
+                let numberValue = entry.repeating ? WI.UIString("~%s", "Approximate Number", "Approximate count of events").format(entry.count) : entry.count;
+                let sourceCodeLocation = entry.callFrame ? entry.callFrame.sourceCodeLocation : entry.sourceCodeLocation;
+                let labelValue = labelForLocation(key, sourceCodeLocation);
+                let followingRow = this._eventHandlersRow;
+
+                let row;
+                if (i === 0) {
+                    row = this._timerInstallationsRow;
+                    this._timerInstallationsNumberElement.textContent = numberValue;
+                    this._timerInstallationsLabelElement.append(labelValue);
+                } else
+                    row = this._insertTableRow(this._sourcesTable, this._sourcesRows, {headerValue, numberValue, labelValue, followingRow});
+
+                if (entry.functionName)
+                    row.querySelector(".label").append(` ${enDash} ${entry.functionName}`);
+
+                if (++i === sectionLimit && sorted.size > sectionLimit) {
+                    this._insertTableRow(this._sourcesTable, this._sourcesRows, {labelValue: createEllipsisElement(), followingRow});
+                    break;
+                }
+            }
+        }
+
+        if (eventHandlerMap.size) {
+            let i = 0;
+            let sorted = sortMapByEntryCountProperty(eventHandlerMap);
+            for (let [key, entry] of sorted) {
+                let numberValue = entry.count;
+                let labelValue = labelForLocation(key, entry.sourceCodeLocation);
+                let followingRow = this._observerHandlersRow;
+
+                if (i === 0) {
+                    this._eventHandlersNumberElement.textContent = numberValue;
+                    this._eventHandlersLabelElement.append(labelValue);
+                } else
+                    this._insertTableRow(this._sourcesTable, this._sourcesRows, {headerValue, numberValue, labelValue, followingRow});
+
+                if (++i === sectionLimit && sorted.size > sectionLimit) {
+                    this._insertTableRow(this._sourcesTable, this._sourcesRows, {labelValue: createEllipsisElement(), followingRow});
+                    break;
+                }
+            }
+        }
+
+        if (observerCallbackMap.size) {
+            let i = 0;
+            let sorted = sortMapByEntryCountProperty(observerCallbackMap);
+            for (let [key, entry] of sorted) {
+                let numberValue = entry.count;
+                let labelValue = labelForLocation(key, entry.sourceCodeLocation);
+
+                if (i === 0) {
+                    this._observerHandlersNumberElement.textContent = numberValue;
+                    this._observerHandlersLabelElement.append(labelValue);
+                } else
+                    this._insertTableRow(this._sourcesTable, this._sourcesRows, {headerValue, numberValue, labelValue});
+
+                if (++i === sectionLimit && sorted.size > sectionLimit) {
+                    this._insertTableRow(this._sourcesTable, this._sourcesRows, {labelValue: createEllipsisElement()});
+                    break;
+                }
+            }
+        }
+    }
+
     _layoutEnergyChart(average, visibleDuration)
     {
         // The lower the bias value [0..1], the more it increases the skew towards rangeHigh.
@@ -696,7 +1104,7 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         }
     }
 
-    _computeSamplingData(startTime, endTime)
+    _computeStatisticsData(startTime, endTime)
     {
         // Compute per-millisecond samples of what the main thread was doing.
         // We construct an array for every millisecond between the start and end time
@@ -717,23 +1125,78 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
 
         const includeRecordBeforeStart = true;
 
+        function incrementTypeCount(map, key) {
+            let entry = map.get(key);
+            if (entry)
+                map.set(key, entry + 1);
+            else
+                map.set(key, 1);
+        }
+
+        let timerInstallationRecords = [];
+        let eventHandlerRecords = [];
+        let observerCallbackRecords = [];
+        let scriptEntries = 0;
+        let timerTypes = new Map;
+        let eventTypes = new Map;
+        let observerTypes = new Map;
+
+        let repeatingTimers = new Map;
+        let possibleRepeatingTimers = new Set;
+
         let scriptTimeline = this._recording.timelineForRecordType(WI.TimelineRecord.Type.Script);
         let scriptRecords = scriptTimeline ? scriptTimeline.recordsInTimeRange(startTime, endTime, includeRecordBeforeStart) : [];
         scriptRecords = scriptRecords.filter((record) => {
+            // Return true for event types that define script entries/exits.
+            // Return false for events with no time ranges or if they are contained in other events.
             switch (record.eventType) {
             case WI.ScriptTimelineRecord.EventType.ScriptEvaluated:
             case WI.ScriptTimelineRecord.EventType.APIScriptEvaluated:
+                scriptEntries++;
+                return true;
+
             case WI.ScriptTimelineRecord.EventType.ObserverCallback:
+                incrementTypeCount(observerTypes, record.details);
+                observerCallbackRecords.push(record);
+                scriptEntries++;
+                return true;
+
             case WI.ScriptTimelineRecord.EventType.EventDispatched:
+                incrementTypeCount(eventTypes, record.details);
+                eventHandlerRecords.push(record);
+                scriptEntries++;
+                return true;
+
             case WI.ScriptTimelineRecord.EventType.MicrotaskDispatched:
+                // Do not normally count this as a script entry, but they may have a time range
+                // that is not covered by script entry (queueMicrotask).
+                return true;
+
             case WI.ScriptTimelineRecord.EventType.TimerFired:
+                incrementTypeCount(timerTypes, "setTimeout");
+                if (possibleRepeatingTimers.has(record.details)) {
+                    let entry = repeatingTimers.get(record.details);
+                    if (entry)
+                        entry.count += 1;
+                    else
+                        repeatingTimers.set(record.details, {record, count: 1});
+                } else
+                    possibleRepeatingTimers.add(record.details);
+                scriptEntries++;
+                return true;
+
             case WI.ScriptTimelineRecord.EventType.AnimationFrameFired:
-                // These event types define script entry/exits.
+                incrementTypeCount(timerTypes, "requestAnimationFrame");
+                scriptEntries++;
                 return true;
 
             case WI.ScriptTimelineRecord.EventType.AnimationFrameRequested:
-            case WI.ScriptTimelineRecord.EventType.AnimationFrameCanceled:
             case WI.ScriptTimelineRecord.EventType.TimerInstalled:
+                // These event types have no time range, or are contained by the others.
+                timerInstallationRecords.push(record);
+                return false;
+
+            case WI.ScriptTimelineRecord.EventType.AnimationFrameCanceled:
             case WI.ScriptTimelineRecord.EventType.TimerRemoved:
             case WI.ScriptTimelineRecord.EventType.ProbeSampleRecorded:
             case WI.ScriptTimelineRecord.EventType.ConsoleProfileRecorded:
@@ -844,6 +1307,14 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
             samplesLayout,
             samplesPaint,
             samplesStyle,
+            scriptEntries,
+            timerTypes,
+            eventTypes,
+            observerTypes,
+            timerInstallationRecords,
+            eventHandlerRecords,
+            observerCallbackRecords,
+            repeatingTimers,
         };
     }
 
@@ -858,6 +1329,132 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         this._workerViews = [];
     }
 
+    _resetSourcesFilters()
+    {
+        if (!this._sourcesFilter)
+            return;
+
+        this._sourcesFilterRow.hidden = true;
+        this._sourcesFilterLabelElement.removeChildren();
+
+        this._timerInstallationsRow.hidden = false;
+        this._eventHandlersRow.hidden = false;
+        this._observerHandlersRow.hidden = false;
+
+        this._sourcesFilter.timer.clear();
+        this._sourcesFilter.event.clear();
+        this._sourcesFilter.observer.clear();
+    }
+
+    _addSourcesFilter(type, name)
+    {
+        this._sourcesFilter[type].add(name);
+        this._updateSourcesFilters();
+    }
+
+    _removeSourcesFilter(type, name)
+    {
+        this._sourcesFilter[type].delete(name);
+        this._updateSourcesFilters();
+    }
+
+    _updateSourcesFilters()
+    {
+        let timerFilters = this._sourcesFilter.timer;
+        let eventFilters = this._sourcesFilter.event;
+        let observerFilters = this._sourcesFilter.observer;
+
+        if (!timerFilters.size && !eventFilters.size && !observerFilters.size) {
+            this._resetSourcesFilters();
+            return;
+        }
+
+        let createActiveFilterElement = (type, name) => {
+            let span = document.createElement("span");
+            span.className = "filter active";
+            span.textContent = name;
+            span.addEventListener("mouseup", (event) => {
+                this._removeSourcesFilter(type, name);
+                this._layoutStatisticsAndSources();
+            });
+            return span;
+        }
+
+        this._sourcesFilterRow.hidden = false;
+        this._sourcesFilterLabelElement.removeChildren();
+
+        for (let name of timerFilters)
+            this._sourcesFilterLabelElement.appendChild(createActiveFilterElement("timer", name));
+        for (let name of eventFilters)
+            this._sourcesFilterLabelElement.appendChild(createActiveFilterElement("event", name));
+        for (let name of observerFilters)
+            this._sourcesFilterLabelElement.appendChild(createActiveFilterElement("observer", name));
+
+        this._timerInstallationsRow.hidden = !timerFilters.size;
+        this._eventHandlersRow.hidden = !eventFilters.size;
+        this._observerHandlersRow.hidden = !observerFilters.size;
+    }
+
+    _createTableRow(table)
+    {
+        let row = table.appendChild(document.createElement("tr"));
+
+        let headerCell = row.appendChild(document.createElement("th"));
+
+        let numberCell = row.appendChild(document.createElement("td"));
+        numberCell.className = "number";
+
+        let labelCell = row.appendChild(document.createElement("td"));
+        labelCell.className = "label";
+
+        return {row, headerCell, numberCell, labelCell};
+    }
+
+    _insertTableRow(table, rowList, {headerValue, numberValue, labelValue, followingRow})
+    {
+        let {row, headerCell, numberCell, labelCell} = this._createTableRow(table);
+        rowList.push(row);
+
+        if (followingRow)
+            table.insertBefore(row, followingRow);
+
+        if (headerValue)
+            headerCell.textContent = headerValue;
+
+        if (numberValue)
+            numberCell.textContent = numberValue;
+
+        if (labelValue)
+            labelCell.append(labelValue);
+
+        return row;
+    }
+
+    _clearStatistics()
+    {
+        this._scriptEntriesNumberElement.textContent = emDash;
+
+        for (let row of this._statisticsRows)
+            row.remove();
+        this._statisticsRows = [];
+    }
+
+    _clearSources()
+    {
+        this._timerInstallationsNumberElement.textContent = emDash;
+        this._timerInstallationsLabelElement.textContent = "";
+
+        this._eventHandlersNumberElement.textContent = emDash;
+        this._eventHandlersLabelElement.textContent = "";
+
+        this._observerHandlersNumberElement.textContent = emDash;
+        this._observerHandlersLabelElement.textContent = "";
+
+        for (let row of this._sourcesRows)
+            row.remove();
+        this._sourcesRows = [];
+    }
+
     _clearEnergyImpactText()
     {
         this._energyImpactLabelElement.classList.remove("low", "medium", "high");
@@ -1009,6 +1606,10 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
     }
 };
 
+WI.CPUTimelineView.LayoutReason = {
+    Internal: Symbol("cpu-timeline-view-internal-layout"),
+};
+
 // NOTE: UI follows this order.
 WI.CPUTimelineView.SampleType = {
     Script: "sample-type-script",
index b087142..54b1413 100644 (file)
@@ -238,7 +238,7 @@ body.docked:matches(.right, .left) #navigation-sidebar.collapsed > .resizer {
 
 .resource-link,
 .go-to-link {
-    color: hsl(0, 0%, 33%);
+    color: var(--link-text-color);
     text-decoration: underline;
     cursor: pointer;
     -webkit-user-select: none;
@@ -477,11 +477,6 @@ body[dir=rtl] .go-to-arrow {
         filter: unset;
     }
 
-    .resource-link,
-    .go-to-link {
-        color: var(--text-color-secondary);
-    }
-
     .expand-list-button {
         color: inherit;
     }
index c592a45..cc4bd09 100644 (file)
     --background-color-content: white;
     --background-color-code: white;
 
+    /* Gray background with lighter foreground. In dark mode this is lighter. */
+    --gray-background-color: hsl(0, 0%, 66%);
+    --gray-foreground-color: white;
+
+    --link-text-color: hsl(0, 0%, 33%);
+
     --selected-foreground-color: white;
     --selected-secondary-text-color: hsla(0, 100%, 100%, 0.7);
     --selected-background-color: hsl(212, 92%, 54%);
@@ -215,6 +221,11 @@ body.window-inactive * {
         --background-color-content: hsl(0, 0%, 21%);
         --background-color-code: hsl(0, 0%, 21%);
 
+        --gray-background-color: hsl(0, 0%, 50%);
+        --gray-foreground-color: hsl(0, 0%, 33%);
+
+        --link-text-color: var(--text-color-secondary);
+
         --background-color-alternate: hsla(0, 0%, var(--foreground-lightness), 0.05);
         --background-color-selected: hsla(0, 0%, var(--foreground-lightness), 0.1);