Web Inspector: Timelines: add a timeline that shows information about any recorded...
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 2 Nov 2019 00:45:35 +0000 (00:45 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 2 Nov 2019 00:45:35 +0000 (00:45 +0000)
https://bugs.webkit.org/show_bug.cgi?id=203651
<rdar://problem/56128726>

Reviewed by Brian Burg.

Source/JavaScriptCore:

Unlike all other forms of Web Animations, CSS animations/transitions, are _not_ created by
JavaScript, and therefore can seemingly appear out of nowhere. This patch expands the Media
timeline to be the Media & Animations timeline, which tracks when CSS animations/transitions
are created, started, delayed, iterated, canceled, or finished.

* CMakeLists.txt:
* DerivedSources-input.xcfilelist:
* DerivedSources.make:
* inspector/protocol/Animation.json: Added.
* inspector/protocol/Timeline.json:
Add an Animation domain for handling the tracking of CSS Web Animations.

Source/WebCore:

Unlike all other forms of Web Animations, CSS animations/transitions, are _not_ created by
JavaScript, and therefore can seemingly appear out of nowhere. This patch expands the Media
timeline to be the Media & Animations timeline, which tracks when CSS animations/transitions
are created, started, delayed, iterated, canceled, or finished.

Test: inspector/animation/tracking.html

* animation/KeyframeEffect.cpp:
(WebCore::KeyframeEffect::apply):
* animation/WebAnimation.h:
* animation/WebAnimation.cpp:
(WebCore::WebAnimation::~WebAnimation):
(WebCore::WebAnimation::contextDestroyed): Added.
(WebCore::WebAnimation::setEffectInternal):
* inspector/InspectorInstrumentation.h:
(WebCore::InspectorInstrumentation::willApplyKeyframeEffect): Added.
(WebCore::InspectorInstrumentation::didChangeWebAnimationEffect): Added.
(WebCore::InspectorInstrumentation::willDestroyWebAnimation): Added.
* inspector/InspectorInstrumentation.cpp:
(WebCore::InspectorInstrumentation::willApplyKeyframeEffectImpl): Added.
(WebCore::InspectorInstrumentation::didChangeWebAnimationEffectImpl): Added.
(WebCore::InspectorInstrumentation::willDestroyWebAnimationImpl): Added.
Add instrumentation points for CSS animations/transitions.

* inspector/agents/InspectorAnimationAgent.h: Added.
* inspector/agents/InspectorAnimationAgent.cpp: Added.
(WebCore::InspectorAnimationAgent::InspectorAnimationAgent):
(WebCore::InspectorAnimationAgent::didCreateFrontendAndBackend):
(WebCore::InspectorAnimationAgent::willDestroyFrontendAndBackend):
(WebCore::InspectorAnimationAgent::startTracking):
(WebCore::InspectorAnimationAgent::stopTracking):
(WebCore::isDelayed):
(WebCore::InspectorAnimationAgent::willApplyKeyframeEffect):
(WebCore::InspectorAnimationAgent::didChangeWebAnimationEffect):
(WebCore::InspectorAnimationAgent::willDestroyWebAnimation):
(WebCore::InspectorAnimationAgent::stopTrackingDeclarativeAnimation):
* inspector/InstrumentingAgents.h:
(WebCore::InstrumentingAgents::persistentInspectorAnimationAgent const): Added.
(WebCore::InstrumentingAgents::setPersistentInspectorAnimationAgent): Added.
(WebCore::InstrumentingAgents::trackingInspectorAnimationAgent const): Added.
(WebCore::InstrumentingAgents::setTrackingInspectorAnimationAgent): Added.
* inspector/InstrumentingAgents.cpp:
(WebCore::InstrumentingAgents::reset):
* inspector/InspectorController.cpp:
(WebCore::InspectorController::createLazyAgents):
Add an Animation domain for handling the tracking of CSS Web Animations.

* inspector/agents/InspectorTimelineAgent.h:
* inspector/agents/InspectorTimelineAgent.cpp:
(WebCore::InspectorTimelineAgent::toggleInstruments):
(WebCore::InspectorTimelineAgent::toggleAnimationInstrument): Added.

* Sources.txt:
* WebCore.xcodeproj/project.pbxproj:

Source/WebInspectorUI:

Unlike all other forms of Web Animations, CSS animations/transitions, are _not_ created by
JavaScript, and therefore can seemingly appear out of nowhere. This patch expands the Media
timeline to be the Media & Animations timeline, which tracks when CSS animations/transitions
are created, started, delayed, iterated, canceled, or finished.

* UserInterface/Protocol/AnimationObserver.js: Added.
(WI.AnimationObserver.prototype.trackingStart):
(WI.AnimationObserver.prototype.trackingUpdate):
(WI.AnimationObserver.prototype.trackingComplete):
* UserInterface/Protocol/Target.js:
(WI.Target.prototype.get AnimationAgent): Added.
* UserInterface/Main.html:
* UserInterface/Base/Main.js:
(WI.loaded):
* UserInterface/Test.html:
* UserInterface/Test/Test.js:
(WI.loaded):
Add an Animation domain for handling the tracking of CSS Web Animations.

* UserInterface/Models/MediaInstrument.js:
(WI.MediaInstrument.prototype.startInstrumentation):
(WI.MediaInstrument.prototype.stopInstrumentation):
(WI.MediaInstrument):
* UserInterface/Models/MediaTimeline.js: Added.
(WI.MediaTimeline.prototype.recordForTrackingAnimationId):
(WI.MediaTimeline.prototype.recordForMediaElementEvents):
(WI.MediaTimeline.prototype.reset):
(WI.MediaTimeline.prototype.addRecord):
* UserInterface/Models/MediaTimelineRecord.js:
(WI.MediaTimelineRecord):
(WI.MediaTimelineRecord.async fromJSON):
(WI.MediaTimelineRecord.prototype.toJSON):
(WI.MediaTimelineRecord.prototype.get trackingAnimationId): Added.
(WI.MediaTimelineRecord.prototype.get timestamps): Added.
(WI.MediaTimelineRecord.prototype.get activeStartTime): Added.
(WI.MediaTimelineRecord.prototype.get updatesDynamically): Added.
(WI.MediaTimelineRecord.prototype.get usesActiveStartTime): Added.
(WI.MediaTimelineRecord.prototype.get displayName):
(WI.MediaTimelineRecord.prototype.get subtitle): Added.
(WI.MediaTimelineRecord.prototype.saveIdentityToCookie):
(WI.MediaTimelineRecord.prototype.updateProgress): Added.
(WI.MediaTimelineRecord.prototype.addDOMEvent): Added.
(WI.MediaTimelineRecord.prototype.powerEfficientPlaybackStateChanged): Added.
(WI.MediaTimelineRecord.prototype._updateTimes): Added.
(WI.MediaTimelineRecord.fromJSON): Deleted.
(WI.MediaTimelineRecord.prototype.get domEvent): Deleted.
(WI.MediaTimelineRecord.prototype.get isPowerEfficient): Deleted.
* UserInterface/Models/Timeline.js:
(WI.Timeline.create):
* UserInterface/Controllers/TimelineManager.js:
(WI.TimelineManager):
(WI.TimelineManager.prototype.async processJSON):
(WI.TimelineManager.prototype.animationTrackingStarted): Added.
(WI.TimelineManager.prototype.animationTrackingUpdated): Added.
(WI.TimelineManager.prototype.animationTrackingCompleted): Added.
(WI.TimelineManager.prototype._updateAutoCaptureInstruments):
(WI.TimelineManager.prototype._handleDOMNodeDidFireEvent):
(WI.TimelineManager.prototype._handleDOMNodePowerEfficientPlaybackStateChanged):
Start/Stop tracking animations based on whether the Media & Animations timeline is enabled.

* UserInterface/Views/MediaTimelineOverviewGraph.js:
(WI.MediaTimelineOverviewGraph):
(WI.MediaTimelineOverviewGraph.get maximumRowCount): Added.
(WI.MediaTimelineOverviewGraph.prototype.reset):
(WI.MediaTimelineOverviewGraph.prototype.layout):
(WI.MediaTimelineOverviewGraph.prototype.updateSelectedRecord):
(WI.MediaTimelineOverviewGraph.prototype._processRecord): Added.
(WI.MediaTimelineOverviewGraph.prototype._processRecord.compareByStartTime): Added.
(WI.MediaTimelineOverviewGraph.prototype._handleRecordAdded):
(WI.MediaTimelineOverviewGraph.prototype._handleTimesUpdated): Added.
(WI.MediaTimelineOverviewGraph.prototype.shown): Deleted.
(WI.MediaTimelineOverviewGraph.prototype.hidden): Deleted.
* UserInterface/Views/MediaTimelineOverviewGraph.css:
(.timeline-overview-graph.media): Added.
(.timeline-overview-graph.media > .graph-row): Added.
(.timeline-overview-graph.media > .graph-row > .timeline-record-bar): Added.
(.timeline-overview-graph.media > .graph-row > .timeline-record-bar:not(.unfinished) > .segment:not(.inactive)): Added.
(.timeline-overview-graph.media:nth-child(even) > .graph-row > .timeline-record-bar:not(.unfinished) > .segment:not(.inactive)): Added.
(.timeline-overview-graph.media > .timeline-record-bar): Deleted.
(.timeline-overview-graph.media > .timeline-record-bar > .segment): Deleted.
* UserInterface/Views/MediaTimelineView.js:
(WI.MediaTimelineView):
(WI.MediaTimelineView.prototype.shown): Added.
(WI.MediaTimelineView.prototype.hidden): Added.
(WI.MediaTimelineView.prototype.closed):
(WI.MediaTimelineView.prototype.reset):
(WI.MediaTimelineView.prototype.dataGridSortComparator):
(WI.MediaTimelineView.prototype.dataGridSortComparator.compareDOMNodes):
(WI.MediaTimelineView.prototype._processPendingRecords):
* UserInterface/Views/MediaTimelineDataGridNode.js:
(WI.MediaTimelineDataGridNode):
(WI.MediaTimelineDataGridNode.prototype.get data):
(WI.MediaTimelineDataGridNode.prototype.createCellContent):
(WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren): Added.
(WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addReadySegment): ADded.
(WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addDelaySegment): ADded.
(WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addActiveSegment): ADded.
(WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addFullScreenSegment): ADded.
(WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addPowerEfficientPlaybackSegment): ADded.
(WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addPausedSegment): ADded.
(WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addPlayingSegment): ADded.
(WI.MediaTimelineDataGridNode.prototype.filterableDataForColumn):
(WI.MediaTimelineDataGridNode.prototype._createNameCellDocumentFragment): Added.
(WI.MediaTimelineDataGridNode.prototype.iconClassNames): Deleted.

 * UserInterface/Views/TimelineRecordBar.js:
(WI.TimelineRecordBar):
(WI.TimelineRecordBar.prototype.refresh):
(WI.TimelineRecordBar.prototype._handleClick):
* UserInterface/Views/TimelineRecordBar.css:
(.timeline-record-bar):
(.timeline-record-bar > :matches(img, .segment)):
(.timeline-record-bar > img):
(.timeline-record-bar > .segment):
(body[dir=ltr] .timeline-record-bar > .segment):
(body[dir=ltr] .timeline-record-bar > .segment:first-of-type):
(body[dir=ltr] .timeline-record-bar > .segment:last-of-type):
(body[dir=rtl] .timeline-record-bar > .segment):
(body[dir=rtl] .timeline-record-bar > .segment:first-of-type):
(body[dir=rtl] .timeline-record-bar > .segment:last-of-type):
(.timeline-record-bar > .segment:not(:last-of-type)):
(.timeline-record-bar.selected > .segment):
(.timeline-record-bar > .segment.inactive,):
(.timeline-record-bar.has-inactive-segment > .segment:not(.inactive)):
(:focus .selected .timeline-record-bar:not(.has-custom-children) > .segment):
(:focus .selected .timeline-record-bar:not(.has-custom-children) > .segment.inactive):
(:focus .selected .timeline-record-bar.has-inactive-segment > .segment:not(.inactive)):
(.timeline-record-bar.timeline-record-type-network > .segment):
(.timeline-record-bar.timeline-record-type-network > .segment.inactive):
(.timeline-record-bar.timeline-record-type-layout > .segment):
(.timeline-record-bar.timeline-record-type-layout.paint > .segment,):
(.timeline-record-bar.timeline-record-type-script > .segment):
(.timeline-record-bar.timeline-record-type-script.garbage-collected > .segment,):
(.timeline-record-bar.timeline-record-type-media > .segment):
(.timeline-record-bar.has-custom-children.timeline-record-type-media > .segment):
(.timeline-record-bar.has-custom-children.timeline-record-type-media > .segment.css-animation-ready):
(.timeline-record-bar.has-custom-children.timeline-record-type-media > .segment:matches(.css-animation-delay, .media-element-paused)):
(.timeline-record-bar.has-custom-children.timeline-record-type-media.media-element > .segment):
(.timeline-record-bar.has-custom-children.timeline-record-type-media.media-element > .segment.media-element-full-screen):
(.timeline-record-bar.has-custom-children.timeline-record-type-media.media-element > .segment.media-element-power-efficient-playback):
(body[dir=ltr] .timeline-record-bar > .segment.inactive,): Deleted.
(body[dir=ltr] .timeline-record-bar.has-inactive-segment > .segment:not(.inactive),): Deleted.
(:focus .selected .timeline-record-bar > .segment): Deleted.
(:focus .selected .timeline-record-bar > .segment.inactive): Deleted.
(body[dir=ltr] :focus .selected .timeline-record-bar.has-inactive-segment > .segment:not(.inactive)): Deleted.
(body[dir=rtl] :focus .selected .timeline-record-bar.has-inactive-segment > .segment:not(.inactive)): Deleted.
Allow timeline record bars to be customized through a delegate callback. If provided, it
will be used instead of any default content. It is expected to return an array of objects,
each having a `startTime` number, `classNames` array, and `title` string. It can also have a
`endTime` number and an `image` string. If `endTime` is `NaN`, the record is unfinished. If
`image` is provided, an `<img>` will be used instead of a segment, allowing for markers.

* UserInterface/Views/TimelineDataGridNode.js:
(WI.TimelineDataGridNode.prototype.createCellContent):
Add a default fallback for `WI.DOMNode` values.

* UserInterface/Views/TimelineTabContentView.js:
(WI.TimelineTabContentView.displayNameForTimelineType):
(WI.TimelineTabContentView.iconClassNameForRecord):
(WI.TimelineTabContentView.displayNameForRecord):
* UserInterface/Views/TimelineRecordTreeElement.js:
* UserInterface/Views/TimelineIcons.css:
(.animation-frame-record .icon):
(.css-animation-record .icon): Added.
(.css-transition-record .icon): Added.
(.media-element-record .icon): Added.
(.animation-record .icon): Deleted.
(.dom-event-record .icon): Deleted.
(.dom-event-record.fullscreen .icon): Deleted.
(.power-efficient-playback-state-changed-record .icon): Deleted.
* UserInterface/Images/DOMEventFullscreen.svg: Removed.
* UserInterface/Images/EventCancel.svg: Added.
* UserInterface/Images/EventIteration.svg: Added.
* UserInterface/Images/EventPause.svg:
* UserInterface/Images/EventPlay.svg:
* UserInterface/Images/EventProcessing.svg:
* UserInterface/Images/EventStop.svg:
* UserInterface/Images/PowerEfficientPlaybackStateChanged.svg: Removed.
* UserInterface/Images/TimelineRecordAnimationFrame.svg: Renamed from Source/WebInspectorUI/UserInterface/Images/TimelineRecordAnimation.svg.
* UserInterface/Images/TimelineRecordCSSAnimation.svg: Added.
* UserInterface/Images/TimelineRecordCSSTransition.svg: Added.
* UserInterface/Images/TimelineRecordMediaElement.svg: Added.
Add new media icons.

* UserInterface/Models/TimelineRecording.js:
(WI.TimelineRecording.async import): Added.
(WI.TimelineRecording.import): Deleted.
* UserInterface/Models/TimelineRecord.js:
* UserInterface/Models/CPUTimelineRecord.js:
* UserInterface/Models/HeapAllocationsTimelineRecord.js:
(WI.HeapAllocationsTimelineRecord.async fromJSON): Added.
(WI.HeapAllocationsTimelineRecord.fromJSON): Deleted.
* UserInterface/Models/LayoutTimelineRecord.js:
* UserInterface/Models/MemoryTimelineRecord.js:
* UserInterface/Models/RenderingFrameTimelineRecord.js:
* UserInterface/Models/ResourceTimelineRecord.js:
* UserInterface/Models/ScriptTimelineRecord.js:
Make the importing of timeline records `async` so we can attempt to rehydrate the DOM nodes
of any media records (as well as wait for heap snapshots).

* UserInterface/Models/DOMNode.js:
(WI.DOMNode):
(WI.DOMNode.prototype.isMediaElement): Added.
(WI.DOMNode.prototype._shouldListenForEventListeners): Deleted.

* Localizations/en.lproj/localizedStrings.js:

Source/WTF:

* wtf/Markable.h:
(WTF::operator==):
(WTF::operator!=):
Add extra utility operators.

Tools:

* TestWebKitAPI/Tests/WTF/Markable.cpp:
(TestWebKitAPI::TEST):
Add tests for extra utility operators.

LayoutTests:

* inspector/animation/tracking.html: Added.
* inspector/animation/tracking-expected.txt: Added.

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

74 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/animation/tracking-expected.txt [new file with mode: 0644]
LayoutTests/inspector/animation/tracking.html [new file with mode: 0644]
Source/JavaScriptCore/CMakeLists.txt
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/DerivedSources-input.xcfilelist
Source/JavaScriptCore/DerivedSources.make
Source/JavaScriptCore/inspector/protocol/Animation.json [new file with mode: 0644]
Source/JavaScriptCore/inspector/protocol/Timeline.json
Source/WTF/ChangeLog
Source/WTF/wtf/Markable.h
Source/WebCore/ChangeLog
Source/WebCore/Sources.txt
Source/WebCore/WebCore.xcodeproj/project.pbxproj
Source/WebCore/animation/KeyframeEffect.cpp
Source/WebCore/animation/WebAnimation.cpp
Source/WebCore/animation/WebAnimation.h
Source/WebCore/inspector/InspectorController.cpp
Source/WebCore/inspector/InspectorInstrumentation.cpp
Source/WebCore/inspector/InspectorInstrumentation.h
Source/WebCore/inspector/InstrumentingAgents.cpp
Source/WebCore/inspector/InstrumentingAgents.h
Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp [new file with mode: 0644]
Source/WebCore/inspector/agents/InspectorAnimationAgent.h [new file with mode: 0644]
Source/WebCore/inspector/agents/InspectorTimelineAgent.cpp
Source/WebCore/inspector/agents/InspectorTimelineAgent.h
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Base/Main.js
Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js
Source/WebInspectorUI/UserInterface/Images/DOMEventFullscreen.svg [deleted file]
Source/WebInspectorUI/UserInterface/Images/EventCancel.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/EventIteration.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/EventPause.svg
Source/WebInspectorUI/UserInterface/Images/EventPlay.svg
Source/WebInspectorUI/UserInterface/Images/EventProcessing.svg
Source/WebInspectorUI/UserInterface/Images/EventStop.svg
Source/WebInspectorUI/UserInterface/Images/PowerEfficientPlaybackStateChanged.svg [deleted file]
Source/WebInspectorUI/UserInterface/Images/TimelineRecordAnimationFrame.svg [moved from Source/WebInspectorUI/UserInterface/Images/TimelineRecordAnimation.svg with 100% similarity]
Source/WebInspectorUI/UserInterface/Images/TimelineRecordCSSAnimation.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/TimelineRecordCSSTransition.svg [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Images/TimelineRecordMediaElement.svg [moved from Source/WebInspectorUI/UserInterface/Images/DOMEvent.svg with 97% similarity]
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/CPUTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/DOMNode.js
Source/WebInspectorUI/UserInterface/Models/HeapAllocationsTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/LayoutTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/MediaInstrument.js
Source/WebInspectorUI/UserInterface/Models/MediaTimeline.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Models/MediaTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/MemoryTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/RenderingFrameTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/ResourceTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/ScriptTimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/Timeline.js
Source/WebInspectorUI/UserInterface/Models/TimelineRecord.js
Source/WebInspectorUI/UserInterface/Models/TimelineRecording.js
Source/WebInspectorUI/UserInterface/Protocol/AnimationObserver.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Protocol/Target.js
Source/WebInspectorUI/UserInterface/Test.html
Source/WebInspectorUI/UserInterface/Test/Test.js
Source/WebInspectorUI/UserInterface/Views/DOMTreeElementPathComponent.js
Source/WebInspectorUI/UserInterface/Views/MediaTimelineDataGridNode.js
Source/WebInspectorUI/UserInterface/Views/MediaTimelineOverviewGraph.css
Source/WebInspectorUI/UserInterface/Views/MediaTimelineOverviewGraph.js
Source/WebInspectorUI/UserInterface/Views/MediaTimelineView.js
Source/WebInspectorUI/UserInterface/Views/TimelineDataGridNode.js
Source/WebInspectorUI/UserInterface/Views/TimelineIcons.css
Source/WebInspectorUI/UserInterface/Views/TimelineRecordBar.css
Source/WebInspectorUI/UserInterface/Views/TimelineRecordBar.js
Source/WebInspectorUI/UserInterface/Views/TimelineRecordTreeElement.js
Source/WebInspectorUI/UserInterface/Views/TimelineTabContentView.js
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WTF/Markable.cpp

index 625083b..16ba7da 100644 (file)
@@ -1,3 +1,14 @@
+2019-11-01  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Timelines: add a timeline that shows information about any recorded CSS animation/transition
+        https://bugs.webkit.org/show_bug.cgi?id=203651
+        <rdar://problem/56128726>
+
+        Reviewed by Brian Burg.
+
+        * inspector/animation/tracking.html: Added.
+        * inspector/animation/tracking-expected.txt: Added.
+
 2019-11-01  Said Abou-Hallawa  <sabouhallawa@apple.com>
 
         SVG pair properties must be detached from their owner before it's deleted
diff --git a/LayoutTests/inspector/animation/tracking-expected.txt b/LayoutTests/inspector/animation/tracking-expected.txt
new file mode 100644 (file)
index 0000000..4f6a048
--- /dev/null
@@ -0,0 +1,16 @@
+Tests that Animation.startTracking and Animation.stopTracking trigger Animation.trackingStart, Animation.trackingUpdate, and Animation.trackingComplete events with expected data.
+
+
+== Running test suite: Animation.Tracking
+-- Running test case: Animation.Tracking.StartAndStopTrackingWithEvent
+Animation.trackingStart
+PASS: Should have a timestamp when starting.
+Animation.trackingUpdate
+PASS: Should should have a timestamp number.
+PASS: Should have an event object.
+PASS: Event should have an animation tracking ID.
+PASS: Event should have an animation state.
+PASS: Event should have a node ID.
+PASS: Event should have the related CSS animation's name.
+Animation.trackingComplete
+
diff --git a/LayoutTests/inspector/animation/tracking.html b/LayoutTests/inspector/animation/tracking.html
new file mode 100644 (file)
index 0000000..79a306e
--- /dev/null
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src="../../http/tests/inspector/resources/protocol-test.js"></script>
+<style>
+@keyframes test {
+    to { opacity: 0; }
+}
+.test {
+    animation: test 1s linear;
+}
+</style>
+<script>
+function startCSSAnimation() {
+    let element = document.body.appendChild(document.createElement("div"));
+    element.className = "test";
+}
+
+function test()
+{
+    let suite = ProtocolTest.createAsyncSuite("Animation.Tracking");
+
+    InspectorProtocol.sendCommand("DOM.getDocument");
+
+    suite.addTestCase({
+        name: "Animation.Tracking.StartAndStopTrackingWithEvent",
+        test(resolve, reject) {
+            InspectorProtocol.awaitEvent({event: "Animation.trackingStart"}).then((messageObject) => {
+                ProtocolTest.log("Animation.trackingStart");
+                ProtocolTest.expectEqual(typeof messageObject.params.timestamp, "number", "Should have a timestamp when starting.");
+
+                ProtocolTest.evaluateInPage("startCSSAnimation()");
+            });
+
+            InspectorProtocol.awaitEvent({event: "Animation.trackingUpdate"}).then((messageObject) => {
+                ProtocolTest.log("Animation.trackingUpdate");
+                ProtocolTest.expectEqual(typeof messageObject.params.timestamp, "number", "Should should have a timestamp number.");
+                ProtocolTest.expectEqual(typeof messageObject.params.event, "object", "Should have an event object.");
+                ProtocolTest.expectEqual(typeof messageObject.params.event.trackingAnimationId, "string", "Event should have an animation tracking ID.");
+                ProtocolTest.expectEqual(typeof messageObject.params.event.animationState, "string", "Event should have an animation state.");
+                ProtocolTest.expectEqual(typeof messageObject.params.event.nodeId, "number", "Event should have a node ID.");
+                ProtocolTest.expectEqual(messageObject.params.event.animationName, "test", "Event should have the related CSS animation's name.");
+
+                ProtocolTest.assert(typeof messageObject.params.event.transitionProperty === "undefined", "Event should not have a `transitionProperty` if it has an `animationName`.");
+
+                InspectorProtocol.sendCommand("Animation.stopTracking", {});
+            });
+
+            InspectorProtocol.awaitEvent({event: "Animation.trackingComplete"}).then((messageObject) => {
+                ProtocolTest.log("Animation.trackingComplete");
+                resolve();
+            });
+
+            InspectorProtocol.sendCommand("Animation.startTracking", {});
+        }
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body onload="runTest()">
+<p>Tests that Animation.startTracking and Animation.stopTracking trigger Animation.trackingStart, Animation.trackingUpdate, and Animation.trackingComplete events with expected data.</p>
+</body>
+</html>
index 94b3dcb..f39a484 100644 (file)
@@ -1140,6 +1140,7 @@ set(JavaScriptCore_INSPECTOR_PROTOCOL_SCRIPTS
 )
 
 set(JavaScriptCore_INSPECTOR_DOMAINS
+    ${JAVASCRIPTCORE_DIR}/inspector/protocol/Animation.json
     ${JAVASCRIPTCORE_DIR}/inspector/protocol/ApplicationCache.json
     ${JAVASCRIPTCORE_DIR}/inspector/protocol/Audit.json
     ${JAVASCRIPTCORE_DIR}/inspector/protocol/CSS.json
index 63fa013..bc0b6c6 100644 (file)
@@ -1,3 +1,23 @@
+2019-11-01  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Timelines: add a timeline that shows information about any recorded CSS animation/transition
+        https://bugs.webkit.org/show_bug.cgi?id=203651
+        <rdar://problem/56128726>
+
+        Reviewed by Brian Burg.
+
+        Unlike all other forms of Web Animations, CSS animations/transitions, are _not_ created by
+        JavaScript, and therefore can seemingly appear out of nowhere. This patch expands the Media
+        timeline to be the Media & Animations timeline, which tracks when CSS animations/transitions
+        are created, started, delayed, iterated, canceled, or finished.
+
+        * CMakeLists.txt:
+        * DerivedSources-input.xcfilelist:
+        * DerivedSources.make:
+        * inspector/protocol/Animation.json: Added.
+        * inspector/protocol/Timeline.json:
+        Add an Animation domain for handling the tracking of CSS Web Animations.
+
 2019-11-01  Saam Barati  <sbarati@apple.com>
 
         Refactor uses of StructureStubInfo 'thisGPR' to a union for thisGPR and prototypeGPR
index 7196e46..86d7b6c 100644 (file)
@@ -77,6 +77,7 @@ $(PROJECT_DIR)/generator/Type.rb
 $(PROJECT_DIR)/generator/Wasm.rb
 $(PROJECT_DIR)/generator/main.rb
 $(PROJECT_DIR)/inspector/InjectedScriptSource.js
+$(PROJECT_DIR)/inspector/protocol/Animation.json
 $(PROJECT_DIR)/inspector/protocol/ApplicationCache.json
 $(PROJECT_DIR)/inspector/protocol/Audit.json
 $(PROJECT_DIR)/inspector/protocol/CPUProfiler.json
index be19b63..f59212f 100644 (file)
@@ -235,6 +235,7 @@ $(BYTECODE_FILES_PATTERNS): $(wildcard $(JavaScriptCore)/generator/*.rb) $(JavaS
 # Inspector interfaces
 
 INSPECTOR_DOMAINS := \
+    $(JavaScriptCore)/inspector/protocol/Animation.json \
     $(JavaScriptCore)/inspector/protocol/ApplicationCache.json \
     $(JavaScriptCore)/inspector/protocol/Audit.json \
     $(JavaScriptCore)/inspector/protocol/CSS.json \
diff --git a/Source/JavaScriptCore/inspector/protocol/Animation.json b/Source/JavaScriptCore/inspector/protocol/Animation.json
new file mode 100644 (file)
index 0000000..beaf8de
--- /dev/null
@@ -0,0 +1,63 @@
+{
+    "domain": "Animation",
+    "description": "Domain for tracking/modifying Web Animations, as well as CSS (declarative) animations and transitions.",
+    "debuggableTypes": ["page", "web-page"],
+    "targetTypes": ["page"],
+    "types": [
+        {
+            "id": "AnimationId",
+            "type": "string",
+            "description": "Unique Web Animation identifier."
+        },
+        {
+            "id": "AnimationState",
+            "type": "string",
+            "enum": ["ready", "delayed", "active", "canceled", "done"]
+        },
+        {
+            "id": "TrackingUpdate",
+            "type": "object",
+            "properties": [
+                { "name": "trackingAnimationId", "$ref": "AnimationId" },
+                { "name": "animationState", "$ref": "AnimationState" },
+                { "name": "nodeId", "$ref": "DOM.NodeId", "optional": true },
+                { "name": "animationName", "type": "string", "optional": true, "description": "Equal to the corresponding `animation-name` CSS property. Should not be provided if `transitionProperty` is also provided." },
+                { "name": "transitionProperty", "type": "string", "optional": true, "description": "Equal to the corresponding `transition-property` CSS property. Should not be provided if `animationName` is also provided." }
+            ]
+        }
+    ],
+    "commands": [
+        {
+            "name": "startTracking",
+            "description": "Start tracking animations. This will produce a `trackingStart` event."
+        },
+        {
+            "name": "stopTracking",
+            "description": "Stop tracking animations. This will produce a `trackingComplete` event."
+        }
+    ],
+    "events": [
+        {
+            "name": "trackingStart",
+            "description": "Dispatched after `startTracking` command.",
+            "parameters": [
+                { "name": "timestamp", "type": "number" }
+            ]
+        },
+        {
+            "name": "trackingUpdate",
+            "description": "Fired for each phase of Web Animation.",
+            "parameters": [
+                { "name": "timestamp", "type": "number" },
+                { "name": "event", "$ref": "TrackingUpdate" }
+            ]
+        },
+        {
+            "name": "trackingComplete",
+            "description": "Dispatched after `stopTracking` command.",
+            "parameters": [
+                { "name": "timestamp", "type": "number" }
+            ]
+        }
+    ]
+}
index b0d3d00..cefd35b 100644 (file)
@@ -42,7 +42,8 @@
                 "Timeline",
                 "CPU",
                 "Memory",
-                "Heap"
+                "Heap",
+                "Animation"
             ]
         },
         {
index d4113fc..eabc405 100644 (file)
@@ -1,3 +1,16 @@
+2019-11-01  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Timelines: add a timeline that shows information about any recorded CSS animation/transition
+        https://bugs.webkit.org/show_bug.cgi?id=203651
+        <rdar://problem/56128726>
+
+        Reviewed by Brian Burg.
+
+        * wtf/Markable.h:
+        (WTF::operator==):
+        (WTF::operator!=):
+        Add extra utility operators.
+
 2019-10-31  Tim Horton  <timothy_horton@apple.com>
 
         Turn on IOSurface support in the iOS Simulator
index c2fb4d5..9ab222b 100644 (file)
@@ -146,6 +146,21 @@ private:
     T m_value;
 };
 
+template <typename T, typename Traits> constexpr bool operator==(const Markable<T, Traits>& x, const Markable<T, Traits>& y)
+{
+    if (bool(x) != bool(y))
+        return false;
+    if (!bool(x))
+        return true;
+    return x.value() == y.value();
+}
+template <typename T, typename Traits> constexpr bool operator==(const Markable<T, Traits>& x, const T& v) { return bool(x) && x.value() == v; }
+template <typename T, typename Traits> constexpr bool operator==(const T& v, const Markable<T, Traits>& x) { return bool(x) && v == x.value(); }
+
+template <typename T, typename Traits> constexpr bool operator!=(const Markable<T, Traits>& x, const Markable<T, Traits>& y) { return !(x == y); }
+template <typename T, typename Traits> constexpr bool operator!=(const Markable<T, Traits>& x, const T& v) { return !(x == v); }
+template <typename T, typename Traits> constexpr bool operator!=(const T& v, const Markable<T, Traits>& x) { return !(v == x); }
+
 } // namespace WTF
 
 using WTF::Markable;
index ae37080..559f5ff 100644 (file)
@@ -1,3 +1,66 @@
+2019-11-01  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Timelines: add a timeline that shows information about any recorded CSS animation/transition
+        https://bugs.webkit.org/show_bug.cgi?id=203651
+        <rdar://problem/56128726>
+
+        Reviewed by Brian Burg.
+
+        Unlike all other forms of Web Animations, CSS animations/transitions, are _not_ created by
+        JavaScript, and therefore can seemingly appear out of nowhere. This patch expands the Media
+        timeline to be the Media & Animations timeline, which tracks when CSS animations/transitions
+        are created, started, delayed, iterated, canceled, or finished.
+
+        Test: inspector/animation/tracking.html
+
+        * animation/KeyframeEffect.cpp:
+        (WebCore::KeyframeEffect::apply):
+        * animation/WebAnimation.h:
+        * animation/WebAnimation.cpp:
+        (WebCore::WebAnimation::~WebAnimation):
+        (WebCore::WebAnimation::contextDestroyed): Added.
+        (WebCore::WebAnimation::setEffectInternal):
+        * inspector/InspectorInstrumentation.h:
+        (WebCore::InspectorInstrumentation::willApplyKeyframeEffect): Added.
+        (WebCore::InspectorInstrumentation::didChangeWebAnimationEffect): Added.
+        (WebCore::InspectorInstrumentation::willDestroyWebAnimation): Added.
+        * inspector/InspectorInstrumentation.cpp:
+        (WebCore::InspectorInstrumentation::willApplyKeyframeEffectImpl): Added.
+        (WebCore::InspectorInstrumentation::didChangeWebAnimationEffectImpl): Added.
+        (WebCore::InspectorInstrumentation::willDestroyWebAnimationImpl): Added.
+        Add instrumentation points for CSS animations/transitions.
+
+        * inspector/agents/InspectorAnimationAgent.h: Added.
+        * inspector/agents/InspectorAnimationAgent.cpp: Added.
+        (WebCore::InspectorAnimationAgent::InspectorAnimationAgent):
+        (WebCore::InspectorAnimationAgent::didCreateFrontendAndBackend):
+        (WebCore::InspectorAnimationAgent::willDestroyFrontendAndBackend):
+        (WebCore::InspectorAnimationAgent::startTracking):
+        (WebCore::InspectorAnimationAgent::stopTracking):
+        (WebCore::isDelayed):
+        (WebCore::InspectorAnimationAgent::willApplyKeyframeEffect):
+        (WebCore::InspectorAnimationAgent::didChangeWebAnimationEffect):
+        (WebCore::InspectorAnimationAgent::willDestroyWebAnimation):
+        (WebCore::InspectorAnimationAgent::stopTrackingDeclarativeAnimation):
+        * inspector/InstrumentingAgents.h:
+        (WebCore::InstrumentingAgents::persistentInspectorAnimationAgent const): Added.
+        (WebCore::InstrumentingAgents::setPersistentInspectorAnimationAgent): Added.
+        (WebCore::InstrumentingAgents::trackingInspectorAnimationAgent const): Added.
+        (WebCore::InstrumentingAgents::setTrackingInspectorAnimationAgent): Added.
+        * inspector/InstrumentingAgents.cpp:
+        (WebCore::InstrumentingAgents::reset):
+        * inspector/InspectorController.cpp:
+        (WebCore::InspectorController::createLazyAgents):
+        Add an Animation domain for handling the tracking of CSS Web Animations.
+
+        * inspector/agents/InspectorTimelineAgent.h:
+        * inspector/agents/InspectorTimelineAgent.cpp:
+        (WebCore::InspectorTimelineAgent::toggleInstruments):
+        (WebCore::InspectorTimelineAgent::toggleAnimationInstrument): Added.
+
+        * Sources.txt:
+        * WebCore.xcodeproj/project.pbxproj:
+
 2019-11-01  Said Abou-Hallawa  <sabouhallawa@apple.com>
 
         SVG pair properties must be detached from their owner before it's deleted
index 9e437f0..a6cdc33 100644 (file)
@@ -1381,6 +1381,7 @@ inspector/WebInjectedScriptManager.cpp
 inspector/WorkerInspectorController.cpp
 inspector/WorkerScriptDebugServer.cpp
 
+inspector/agents/InspectorAnimationAgent.cpp
 inspector/agents/InspectorApplicationCacheAgent.cpp
 inspector/agents/InspectorCPUProfilerAgent.cpp
 inspector/agents/InspectorCSSAgent.cpp
index 949b09d..f5856fc 100644 (file)
                9109E9C8222CABCA00BB6265 /* JSInspectorAuditResourcesObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 9109E9C5222CABC800BB6265 /* JSInspectorAuditResourcesObject.h */; };
                91278D5E21DEDAD600B57184 /* PageAuditAgent.h in Headers */ = {isa = PBXBuildFile; fileRef = 91278D5C21DEDAD500B57184 /* PageAuditAgent.h */; };
                91278D6221DEDAF000B57184 /* WorkerAuditAgent.h in Headers */ = {isa = PBXBuildFile; fileRef = 91278D6021DEDAF000B57184 /* WorkerAuditAgent.h */; };
+               913FE59C2362799A00F9446A /* InspectorAnimationAgent.h in Headers */ = {isa = PBXBuildFile; fileRef = 913FE59A2362799900F9446A /* InspectorAnimationAgent.h */; };
                9175CE5C21E281ED00DF2C27 /* InspectorAuditDOMObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 9175CE5821E281EC00DF2C27 /* InspectorAuditDOMObject.h */; };
                9175CE5C21E281ED00DF2C28 /* JSInspectorAuditDOMObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 9175CE5821E281EC00DF2C28 /* JSInspectorAuditDOMObject.h */; };
                9175CE5E21E281ED00DF2C27 /* InspectorAuditAccessibilityObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 9175CE5A21E281ED00DF2C27 /* InspectorAuditAccessibilityObject.h */; };
                91278D5C21DEDAD500B57184 /* PageAuditAgent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PageAuditAgent.h; sourceTree = "<group>"; };
                91278D5F21DEDAEF00B57184 /* WorkerAuditAgent.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WorkerAuditAgent.cpp; sourceTree = "<group>"; };
                91278D6021DEDAF000B57184 /* WorkerAuditAgent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WorkerAuditAgent.h; sourceTree = "<group>"; };
+               913FE5982362799900F9446A /* InspectorAnimationAgent.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = InspectorAnimationAgent.cpp; path = inspector/agents/InspectorAnimationAgent.cpp; sourceTree = SOURCE_ROOT; };
+               913FE59A2362799900F9446A /* InspectorAnimationAgent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InspectorAnimationAgent.h; path = inspector/agents/InspectorAnimationAgent.h; sourceTree = SOURCE_ROOT; };
                9175CE5721E281EB00DF2C27 /* InspectorAuditDOMObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = InspectorAuditDOMObject.cpp; sourceTree = "<group>"; };
                9175CE5721E281EB00DF2C28 /* JSInspectorAuditDOMObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSInspectorAuditDOMObject.cpp; sourceTree = "<group>"; };
                9175CE5821E281EC00DF2C27 /* InspectorAuditDOMObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InspectorAuditDOMObject.h; sourceTree = "<group>"; };
                CD4E4E722357B558007895C3 /* JSMediaCapabilities.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = JSMediaCapabilities.cpp; sourceTree = "<group>"; };
                CD4E4E7C2357B5FD007895C3 /* JSAudioConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSAudioConfiguration.h; sourceTree = "<group>"; };
                CD4E4E7D2357B5FD007895C3 /* JSMediaCapabilitiesInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSMediaCapabilitiesInfo.h; sourceTree = "<group>"; };
-               CD4E4E7E2357B5FD007895C3 /* JSMediaConfiguration.dep */ = {isa = PBXFileReference; lastKnownFileType = file; path = JSMediaConfiguration.dep; sourceTree = "<group>"; };
+               CD4E4E7E2357B5FD007895C3 /* JSMediaConfiguration.dep */ = {isa = PBXFileReference; lastKnownFileType = text; path = JSMediaConfiguration.dep; sourceTree = "<group>"; };
                CD4E4E7F2357B5FD007895C3 /* JSTransferFunction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSTransferFunction.h; sourceTree = "<group>"; };
                CD4E4E802357B5FE007895C3 /* JSMediaEncodingConfiguration.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = JSMediaEncodingConfiguration.cpp; sourceTree = "<group>"; };
                CD4E4E812357B5FE007895C3 /* JSHdrMetadataType.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = JSHdrMetadataType.cpp; sourceTree = "<group>"; };
                        children = (
                                A5B81CB91FAA44820037D1E6 /* page */,
                                A5B81CC61FAA44C30037D1E6 /* worker */,
+                               913FE5982362799900F9446A /* InspectorAnimationAgent.cpp */,
+                               913FE59A2362799900F9446A /* InspectorAnimationAgent.h */,
                                A5B81CA01FAA44270037D1E6 /* InspectorApplicationCacheAgent.cpp */,
                                A5B81C971FAA44260037D1E6 /* InspectorApplicationCacheAgent.h */,
                                A5B81C961FAA44260037D1E6 /* InspectorCanvasAgent.cpp */,
                                93309DEE099E64920056E581 /* InsertNodeBeforeCommand.h in Headers */,
                                93309DF0099E64920056E581 /* InsertParagraphSeparatorCommand.h in Headers */,
                                93309DF2099E64920056E581 /* InsertTextCommand.h in Headers */,
+                               913FE59C2362799A00F9446A /* InspectorAnimationAgent.h in Headers */,
                                A5B81CA71FAA44620037D1E6 /* InspectorApplicationCacheAgent.h in Headers */,
                                9175CE5E21E281ED00DF2C27 /* InspectorAuditAccessibilityObject.h in Headers */,
                                9175CE5C21E281ED00DF2C27 /* InspectorAuditDOMObject.h in Headers */,
                                BCE1C43C0D9830D3003B02F2 /* JSLocation.h in Headers */,
                                93A806201E03B585008A1F26 /* JSLongRange.h in Headers */,
                                CDAB6D2E17C814EE00C60B34 /* JSMediaControlsHost.h in Headers */,
-                               E4ABAC06236B016D00FA4345 /* StyleBuilder.h in Headers */,
                                159741DB1B7D140100201C92 /* JSMediaDeviceInfo.h in Headers */,
                                15739BBB1B42012D00D258C1 /* JSMediaDevices.h in Headers */,
                                FD23A12613F5FA5900F67001 /* JSMediaElementAudioSourceNode.h in Headers */,
                                414B82051D6DF0E50077EBE3 /* StructuredClone.h in Headers */,
                                BC5EB6A30E81DC4F00B25965 /* StyleBackgroundData.h in Headers */,
                                BC5EB67B0E81D3BE00B25965 /* StyleBoxData.h in Headers */,
+                               E4ABAC06236B016D00FA4345 /* StyleBuilder.h in Headers */,
                                83B9687B19F8AB83004EF7AF /* StyleBuilderConverter.h in Headers */,
                                835D363719FF6193004C93AB /* StyleBuilderCustom.h in Headers */,
+                               E4ABAC04236AE95900FA4345 /* StyleBuilderGenerated.h in Headers */,
                                E4ABABF32368B95900FA4345 /* StyleBuilderState.h in Headers */,
                                BCEF444D0E674628001C1287 /* StyleCachedImage.h in Headers */,
                                E401E0A41C3C0B8300F34D10 /* StyleChange.h in Headers */,
                                0854B0231255E4E600B9CDD0 /* SVGRootInlineBox.h in Headers */,
                                B2227AA30D00BF220071B782 /* SVGScriptElement.h in Headers */,
                                B2227AA60D00BF220071B782 /* SVGSetElement.h in Headers */,
-                               E4ABAC04236AE95900FA4345 /* StyleBuilderGenerated.h in Headers */,
                                E4AFD0100DAF335500F5F55C /* SVGSMILElement.h in Headers */,
                                B2227AA90D00BF220071B782 /* SVGStopElement.h in Headers */,
                                B2227AAC0D00BF220071B782 /* SVGStringList.h in Headers */,
index af7a482..69edd84 100644 (file)
@@ -39,6 +39,7 @@
 #include "FontCascade.h"
 #include "FrameView.h"
 #include "GeometryUtilities.h"
+#include "InspectorInstrumentation.h"
 #include "JSCompositeOperation.h"
 #include "JSCompositeOperationOrAuto.h"
 #include "JSDOMConvert.h"
@@ -1019,11 +1020,14 @@ void KeyframeEffect::apply(RenderStyle& targetStyle)
 
     updateAcceleratedAnimationState();
 
-    auto progress = getComputedTiming().progress;
-    if (!progress)
+    auto computedTiming = getComputedTiming();
+
+    InspectorInstrumentation::willApplyKeyframeEffect(*m_target, *this, computedTiming);
+
+    if (!computedTiming.progress)
         return;
 
-    setAnimatedPropertiesInStyle(targetStyle, progress.value());
+    setAnimatedPropertiesInStyle(targetStyle, computedTiming.progress.value());
 
     // https://w3c.github.io/web-animations/#side-effects-section
     // For every property targeted by at least one animation effect that is current or in effect, the user agent
index 0fd2cae..19834d0 100644 (file)
@@ -34,6 +34,7 @@
 #include "Document.h"
 #include "DocumentTimeline.h"
 #include "EventNames.h"
+#include "InspectorInstrumentation.h"
 #include "JSWebAnimation.h"
 #include "KeyframeEffect.h"
 #include "Microtasks.h"
@@ -75,10 +76,19 @@ WebAnimation::WebAnimation(Document& document)
 
 WebAnimation::~WebAnimation()
 {
+    InspectorInstrumentation::willDestroyWebAnimation(*this);
+
     if (m_timeline)
         m_timeline->forgetAnimation(this);
 }
 
+void WebAnimation::contextDestroyed()
+{
+    InspectorInstrumentation::willDestroyWebAnimation(*this);
+
+    ActiveDOMObject::contextDestroyed();
+}
+
 void WebAnimation::remove()
 {
     // This object could be deleted after either clearing the effect or timeline relationship.
@@ -174,6 +184,8 @@ void WebAnimation::setEffectInternal(RefPtr<AnimationEffect>&& newEffect, bool d
         if (m_timeline && newTarget && previousTarget != newTarget)
             m_timeline->animationWasAddedToElement(*this, *newTarget);
     }
+
+    InspectorInstrumentation::didChangeWebAnimationEffect(*this);
 }
 
 void WebAnimation::setTimeline(RefPtr<AnimationTimeline>&& timeline)
index 1c11dcf..8b86ae3 100644 (file)
@@ -135,6 +135,10 @@ public:
 
     bool hasPendingActivity() const final;
 
+    // ContextDestructionObserver.
+    ScriptExecutionContext* scriptExecutionContext() const final { return ActiveDOMObject::scriptExecutionContext(); }
+    void contextDestroyed() final;
+
     using RefCounted::ref;
     using RefCounted::deref;
 
@@ -204,7 +208,6 @@ private:
     EventTargetInterface eventTargetInterface() const final { return WebAnimationEventTargetInterfaceType; }
     void refEventTarget() final { ref(); }
     void derefEventTarget() final { deref(); }
-    ScriptExecutionContext* scriptExecutionContext() const final { return ActiveDOMObject::scriptExecutionContext(); }
 };
 
 } // namespace WebCore
index 81093d3..7d50ddc 100644 (file)
@@ -38,6 +38,7 @@
 #include "DOMWrapperWorld.h"
 #include "Frame.h"
 #include "GraphicsContext.h"
+#include "InspectorAnimationAgent.h"
 #include "InspectorApplicationCacheAgent.h"
 #include "InspectorCPUProfilerAgent.h"
 #include "InspectorCSSAgent.h"
@@ -185,6 +186,7 @@ void InspectorController::createLazyAgents()
     m_agents.append(makeUnique<PageAuditAgent>(pageContext));
     m_agents.append(makeUnique<InspectorCanvasAgent>(pageContext));
     m_agents.append(makeUnique<InspectorTimelineAgent>(pageContext));
+    m_agents.append(makeUnique<InspectorAnimationAgent>(pageContext));
 
     if (auto& commandLineAPIHost = m_injectedScriptManager->commandLineAPIHost())
         commandLineAPIHost->init(m_instrumentingAgents.copyRef());
index 717f01a..4b7bec1 100644 (file)
@@ -33,6 +33,7 @@
 #include "InspectorInstrumentation.h"
 
 #include "CachedResource.h"
+#include "ComputedEffectTiming.h"
 #include "CustomHeaderFields.h"
 #include "DOMWindow.h"
 #include "DOMWrapperWorld.h"
@@ -54,6 +55,7 @@
 #include "InspectorTimelineAgent.h"
 #include "InspectorWorkerAgent.h"
 #include "InstrumentingAgents.h"
+#include "KeyframeEffect.h"
 #include "LoaderStrategy.h"
 #include "PageDOMDebuggerAgent.h"
 #include "PageDebuggerAgent.h"
@@ -1109,6 +1111,24 @@ InstrumentingAgents* InspectorInstrumentation::instrumentingAgentsForWebGPUDevic
 }
 #endif
 
+void InspectorInstrumentation::willApplyKeyframeEffectImpl(InstrumentingAgents& instrumentingAgents, Element& target, KeyframeEffect& effect, ComputedEffectTiming computedTiming)
+{
+    if (auto* animationAgent = instrumentingAgents.trackingInspectorAnimationAgent())
+        animationAgent->willApplyKeyframeEffect(target, effect, computedTiming);
+}
+
+void InspectorInstrumentation::didChangeWebAnimationEffectImpl(InstrumentingAgents& instrumentingAgents, WebAnimation& animation)
+{
+    if (auto* animationAgent = instrumentingAgents.trackingInspectorAnimationAgent())
+        animationAgent->didChangeWebAnimationEffect(animation);
+}
+
+void InspectorInstrumentation::willDestroyWebAnimationImpl(InstrumentingAgents& instrumentingAgents, WebAnimation& animation)
+{
+    if (auto* animationAgent = instrumentingAgents.trackingInspectorAnimationAgent())
+        animationAgent->willDestroyWebAnimation(animation);
+}
+
 #if ENABLE(RESOURCE_USAGE)
 void InspectorInstrumentation::didHandleMemoryPressureImpl(InstrumentingAgents& instrumentingAgents, Critical critical)
 {
index 466876d..6698431 100644 (file)
@@ -36,6 +36,7 @@
 #include "CanvasBase.h"
 #include "CanvasRenderingContext.h"
 #include "Database.h"
+#include "DeclarativeAnimation.h"
 #include "DocumentThreadableLoader.h"
 #include "Element.h"
 #include "EventTarget.h"
@@ -45,6 +46,7 @@
 #include "InspectorInstrumentationPublic.h"
 #include "Page.h"
 #include "StorageArea.h"
+#include "WebAnimation.h"
 #include <JavaScriptCore/ConsoleMessage.h>
 #include <initializer_list>
 #include <wtf/CompletionHandler.h>
@@ -78,6 +80,7 @@ class EventListener;
 class HTTPHeaderMap;
 class InspectorTimelineAgent;
 class InstrumentingAgents;
+class KeyframeEffect;
 class NetworkLoadMetrics;
 class Node;
 class PseudoElement;
@@ -107,6 +110,7 @@ class WebGPUSwapChain;
 
 enum class StorageType;
 
+struct ComputedEffectTiming;
 struct WebSocketFrame;
 
 class InspectorInstrumentation {
@@ -299,6 +303,10 @@ public:
     static void willDestroyWebGPUPipeline(WebGPUPipeline&);
 #endif
 
+    static void willApplyKeyframeEffect(Element&, KeyframeEffect&, ComputedEffectTiming);
+    static void didChangeWebAnimationEffect(WebAnimation&);
+    static void willDestroyWebAnimation(WebAnimation&);
+
     static void networkStateChanged(Page&);
     static void updateApplicationCacheStatus(Frame*);
 
@@ -494,6 +502,10 @@ private:
     static InstrumentingAgents* instrumentingAgentsForWebGPUDevice(WebGPUDevice&);
 #endif
 
+    static void willApplyKeyframeEffectImpl(InstrumentingAgents&, Element&, KeyframeEffect&, ComputedEffectTiming);
+    static void didChangeWebAnimationEffectImpl(InstrumentingAgents&, WebAnimation&);
+    static void willDestroyWebAnimationImpl(InstrumentingAgents&, WebAnimation&);
+
     static void layerTreeDidChangeImpl(InstrumentingAgents&);
     static void renderLayerDestroyedImpl(InstrumentingAgents&, const RenderLayer&);
 
@@ -1445,6 +1457,27 @@ inline void InspectorInstrumentation::willDestroyWebGPUPipeline(WebGPUPipeline&
 }
 #endif
 
+inline void InspectorInstrumentation::willApplyKeyframeEffect(Element& target, KeyframeEffect& effect, ComputedEffectTiming computedTiming)
+{
+    FAST_RETURN_IF_NO_FRONTENDS(void());
+    if (auto* instrumentingAgents = instrumentingAgentsForDocument(target.document()))
+        willApplyKeyframeEffectImpl(*instrumentingAgents, target, effect, computedTiming);
+}
+
+inline void InspectorInstrumentation::didChangeWebAnimationEffect(WebAnimation& animation)
+{
+    FAST_RETURN_IF_NO_FRONTENDS(void());
+    if (auto* instrumentingAgents = instrumentingAgentsForContext(animation.scriptExecutionContext()))
+        didChangeWebAnimationEffectImpl(*instrumentingAgents, animation);
+}
+
+inline void InspectorInstrumentation::willDestroyWebAnimation(WebAnimation& animation)
+{
+    FAST_RETURN_IF_NO_FRONTENDS(void());
+    if (auto* instrumentingAgents = instrumentingAgentsForContext(animation.scriptExecutionContext()))
+        willDestroyWebAnimationImpl(*instrumentingAgents, animation);
+}
+
 inline void InspectorInstrumentation::networkStateChanged(Page& page)
 {
     FAST_RETURN_IF_NO_FRONTENDS(void());
index 7fd9876..1f9a0d7 100644 (file)
@@ -69,6 +69,8 @@ void InstrumentingAgents::reset()
     m_inspectorDOMDebuggerAgent = nullptr;
     m_pageDOMDebuggerAgent = nullptr;
     m_inspectorCanvasAgent = nullptr;
+    m_persistentInspectorAnimationAgent = nullptr;
+    m_trackingInspectorAnimationAgent = nullptr;
 }
 
 } // namespace WebCore
index 1bd490c..f6fea7d 100644 (file)
@@ -44,6 +44,7 @@ class InspectorScriptProfilerAgent;
 
 namespace WebCore {
 
+class InspectorAnimationAgent;
 class InspectorApplicationCacheAgent;
 class InspectorCPUProfilerAgent;
 class InspectorCSSAgent;
@@ -152,6 +153,12 @@ public:
     InspectorWorkerAgent* inspectorWorkerAgent() const { return m_inspectorWorkerAgent; }
     void setInspectorWorkerAgent(InspectorWorkerAgent* agent) { m_inspectorWorkerAgent = agent; }
 
+    InspectorAnimationAgent* persistentInspectorAnimationAgent() const { return m_persistentInspectorAnimationAgent; }
+    void setPersistentInspectorAnimationAgent(InspectorAnimationAgent* agent) { m_persistentInspectorAnimationAgent = agent; }
+
+    InspectorAnimationAgent* trackingInspectorAnimationAgent() const { return m_trackingInspectorAnimationAgent; }
+    void setTrackingInspectorAnimationAgent(InspectorAnimationAgent* agent) { m_trackingInspectorAnimationAgent = agent; }
+
 private:
     InstrumentingAgents(Inspector::InspectorEnvironment&);
 
@@ -182,6 +189,8 @@ private:
     InspectorDOMDebuggerAgent* m_inspectorDOMDebuggerAgent { nullptr };
     PageDOMDebuggerAgent* m_pageDOMDebuggerAgent { nullptr };
     InspectorCanvasAgent* m_inspectorCanvasAgent { nullptr };
+    InspectorAnimationAgent* m_persistentInspectorAnimationAgent { nullptr };
+    InspectorAnimationAgent* m_trackingInspectorAnimationAgent { nullptr };
 };
 
 } // namespace WebCore
diff --git a/Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp b/Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp
new file mode 100644 (file)
index 0000000..de9a2c5
--- /dev/null
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2016 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+#include "InspectorAnimationAgent.h"
+
+#include "AnimationEffect.h"
+#include "AnimationEffectPhase.h"
+#include "CSSAnimation.h"
+#include "CSSTransition.h"
+#include "DeclarativeAnimation.h"
+#include "Element.h"
+#include "Event.h"
+#include "InspectorDOMAgent.h"
+#include "InstrumentingAgents.h"
+#include "KeyframeEffect.h"
+#include "WebAnimation.h"
+#include <JavaScriptCore/IdentifiersFactory.h>
+#include <JavaScriptCore/InspectorEnvironment.h>
+#include <wtf/HashMap.h>
+#include <wtf/Seconds.h>
+#include <wtf/Stopwatch.h>
+#include <wtf/text/WTFString.h>
+
+namespace WebCore {
+
+using namespace Inspector;
+
+InspectorAnimationAgent::InspectorAnimationAgent(PageAgentContext& context)
+    : InspectorAgentBase("Animation"_s, context)
+    , m_frontendDispatcher(makeUnique<Inspector::AnimationFrontendDispatcher>(context.frontendRouter))
+    , m_backendDispatcher(Inspector::AnimationBackendDispatcher::create(context.backendDispatcher, this))
+{
+}
+
+InspectorAnimationAgent::~InspectorAnimationAgent() = default;
+
+void InspectorAnimationAgent::didCreateFrontendAndBackend(FrontendRouter*, BackendDispatcher*)
+{
+    ASSERT(m_instrumentingAgents.persistentInspectorAnimationAgent() != this);
+    m_instrumentingAgents.setPersistentInspectorAnimationAgent(this);
+}
+
+void InspectorAnimationAgent::willDestroyFrontendAndBackend(DisconnectReason)
+{
+    ErrorString ignored;
+    stopTracking(ignored);
+
+    ASSERT(m_instrumentingAgents.persistentInspectorAnimationAgent() == this);
+    m_instrumentingAgents.setPersistentInspectorAnimationAgent(nullptr);
+}
+
+void InspectorAnimationAgent::startTracking(ErrorString& errorString)
+{
+    if (m_instrumentingAgents.trackingInspectorAnimationAgent() == this) {
+        errorString = "Animation domain already tracking"_s;
+        return;
+    }
+
+    m_instrumentingAgents.setTrackingInspectorAnimationAgent(this);
+
+    ASSERT(m_trackedDeclarativeAnimationData.isEmpty());
+
+    m_frontendDispatcher->trackingStart(m_environment.executionStopwatch()->elapsedTime().seconds());
+}
+
+void InspectorAnimationAgent::stopTracking(ErrorString&)
+{
+    if (m_instrumentingAgents.trackingInspectorAnimationAgent() != this)
+        return;
+
+    m_instrumentingAgents.setTrackingInspectorAnimationAgent(nullptr);
+
+    m_trackedDeclarativeAnimationData.clear();
+
+    m_frontendDispatcher->trackingComplete(m_environment.executionStopwatch()->elapsedTime().seconds());
+}
+
+static bool isDelayed(ComputedEffectTiming& computedTiming)
+{
+    if (!computedTiming.localTime)
+        return false;
+    return computedTiming.localTime.value() < (computedTiming.endTime - computedTiming.activeDuration);
+}
+
+void InspectorAnimationAgent::willApplyKeyframeEffect(Element& target, KeyframeEffect& keyframeEffect, ComputedEffectTiming computedTiming)
+{
+    auto* animation = keyframeEffect.animation();
+    if (!is<DeclarativeAnimation>(animation))
+        return;
+
+    auto ensureResult = m_trackedDeclarativeAnimationData.ensure(downcast<DeclarativeAnimation>(animation), [&] () -> TrackedDeclarativeAnimationData {
+        return { makeString("animation:"_s, IdentifiersFactory::createIdentifier()), computedTiming };
+    });
+    auto& trackingData = ensureResult.iterator->value;
+
+    Optional<Inspector::Protocol::Animation::AnimationState> animationAnimationState;
+
+    if ((ensureResult.isNewEntry || !isDelayed(trackingData.lastComputedTiming)) && isDelayed(computedTiming))
+        animationAnimationState = Inspector::Protocol::Animation::AnimationState::Delayed;
+    else if (ensureResult.isNewEntry || trackingData.lastComputedTiming.phase != computedTiming.phase) {
+        switch (computedTiming.phase) {
+        case AnimationEffectPhase::Before:
+            animationAnimationState = Inspector::Protocol::Animation::AnimationState::Ready;
+            break;
+
+        case AnimationEffectPhase::Active:
+            animationAnimationState = Inspector::Protocol::Animation::AnimationState::Active;
+            break;
+
+        case AnimationEffectPhase::After:
+            animationAnimationState = Inspector::Protocol::Animation::AnimationState::Done;
+            break;
+
+        case AnimationEffectPhase::Idle:
+            animationAnimationState = Inspector::Protocol::Animation::AnimationState::Canceled;
+            break;
+        }
+    } else if (trackingData.lastComputedTiming.currentIteration != computedTiming.currentIteration) {
+        // Iterations are represented by sequential "active" state events.
+        animationAnimationState = Inspector::Protocol::Animation::AnimationState::Active;
+    }
+
+    trackingData.lastComputedTiming = computedTiming;
+
+    if (!animationAnimationState)
+        return;
+
+    auto event = Inspector::Protocol::Animation::TrackingUpdate::create()
+        .setTrackingAnimationId(trackingData.trackingAnimationId)
+        .setAnimationState(animationAnimationState.value())
+        .release();
+
+    if (ensureResult.isNewEntry) {
+        if (auto* domAgent = m_instrumentingAgents.inspectorDOMAgent()) {
+            if (auto nodeId = domAgent->pushNodeToFrontend(&target))
+                event->setNodeId(nodeId);
+        }
+
+        if (is<CSSAnimation>(animation))
+            event->setAnimationName(downcast<CSSAnimation>(*animation).animationName());
+        else if (is<CSSTransition>(animation))
+            event->setTransitionProperty(downcast<CSSTransition>(*animation).transitionProperty());
+        else
+            ASSERT_NOT_REACHED();
+    }
+
+    m_frontendDispatcher->trackingUpdate(m_environment.executionStopwatch()->elapsedTime().seconds(), WTFMove(event));
+}
+
+void InspectorAnimationAgent::didChangeWebAnimationEffect(WebAnimation& animation)
+{
+    if (is<DeclarativeAnimation>(animation))
+        stopTrackingDeclarativeAnimation(downcast<DeclarativeAnimation>(animation));
+}
+
+void InspectorAnimationAgent::willDestroyWebAnimation(WebAnimation& animation)
+{
+    if (is<DeclarativeAnimation>(animation))
+        stopTrackingDeclarativeAnimation(downcast<DeclarativeAnimation>(animation));
+}
+
+void InspectorAnimationAgent::stopTrackingDeclarativeAnimation(DeclarativeAnimation& animation)
+{
+    auto it = m_trackedDeclarativeAnimationData.find(&animation);
+    if (it == m_trackedDeclarativeAnimationData.end())
+        return;
+
+    if (it->value.lastComputedTiming.phase != AnimationEffectPhase::After && it->value.lastComputedTiming.phase != AnimationEffectPhase::Idle) {
+        auto event = Inspector::Protocol::Animation::TrackingUpdate::create()
+            .setTrackingAnimationId(it->value.trackingAnimationId)
+            .setAnimationState(Inspector::Protocol::Animation::AnimationState::Canceled)
+            .release();
+        m_frontendDispatcher->trackingUpdate(m_environment.executionStopwatch()->elapsedTime().seconds(), WTFMove(event));
+    }
+
+    m_trackedDeclarativeAnimationData.remove(it);
+}
+
+} // namespace WebCore
diff --git a/Source/WebCore/inspector/agents/InspectorAnimationAgent.h b/Source/WebCore/inspector/agents/InspectorAnimationAgent.h
new file mode 100644 (file)
index 0000000..7a24bbc
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "ComputedEffectTiming.h"
+#include "InspectorWebAgentBase.h"
+#include <JavaScriptCore/InspectorBackendDispatchers.h>
+#include <JavaScriptCore/InspectorFrontendDispatchers.h>
+#include <wtf/Forward.h>
+
+namespace WebCore {
+
+class DeclarativeAnimation;
+class Element;
+class Event;
+class KeyframeEffect;
+class WebAnimation;
+
+typedef String ErrorString;
+
+class InspectorAnimationAgent final : public InspectorAgentBase, public Inspector::AnimationBackendDispatcherHandler {
+    WTF_MAKE_NONCOPYABLE(InspectorAnimationAgent);
+    WTF_MAKE_FAST_ALLOCATED;
+public:
+    InspectorAnimationAgent(PageAgentContext&);
+    ~InspectorAnimationAgent() override;
+
+    // InspectorAgentBase
+    void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*) override;
+    void willDestroyFrontendAndBackend(Inspector::DisconnectReason) override;
+
+    // AnimationBackendDispatcherHandler
+    void startTracking(ErrorString&) override;
+    void stopTracking(ErrorString&) override;
+
+    // InspectorInstrumentation
+    void willApplyKeyframeEffect(Element&, KeyframeEffect&, ComputedEffectTiming);
+    void didChangeWebAnimationEffect(WebAnimation&);
+    void willDestroyWebAnimation(WebAnimation&);
+
+private:
+    void stopTrackingDeclarativeAnimation(DeclarativeAnimation&);
+
+    std::unique_ptr<Inspector::AnimationFrontendDispatcher> m_frontendDispatcher;
+    RefPtr<Inspector::AnimationBackendDispatcher> m_backendDispatcher;
+
+    struct TrackedDeclarativeAnimationData {
+        String trackingAnimationId;
+        ComputedEffectTiming lastComputedTiming;
+    };
+    HashMap<DeclarativeAnimation*, TrackedDeclarativeAnimationData> m_trackedDeclarativeAnimationData;
+};
+
+} // namespace WebCore
index e53b4ce..cc57d42 100644 (file)
@@ -36,6 +36,7 @@
 #include "DOMWindow.h"
 #include "Event.h"
 #include "Frame.h"
+#include "InspectorAnimationAgent.h"
 #include "InspectorCPUProfilerAgent.h"
 #include "InspectorClient.h"
 #include "InspectorController.h"
@@ -558,6 +559,9 @@ void InspectorTimelineAgent::toggleInstruments(InstrumentState state)
         case Inspector::Protocol::Timeline::Instrument::Timeline:
             toggleTimelineInstrument(state);
             break;
+        case Inspector::Protocol::Timeline::Instrument::Animation:
+            toggleAnimationInstrument(state);
+            break;
         }
     }
 }
@@ -624,6 +628,17 @@ void InspectorTimelineAgent::toggleTimelineInstrument(InstrumentState state)
         internalStop();
 }
 
+void InspectorTimelineAgent::toggleAnimationInstrument(InstrumentState state)
+{
+    if (auto* animationAgent = m_instrumentingAgents.persistentInspectorAnimationAgent()) {
+        ErrorString ignored;
+        if (state == InstrumentState::Start)
+            animationAgent->startTracking(ignored);
+        else
+            animationAgent->stopTracking(ignored);
+    }
+}
+
 void InspectorTimelineAgent::didRequestAnimationFrame(int callbackId, Frame* frame)
 {
     appendRecord(TimelineRecordFactory::createAnimationFrameData(callbackId), TimelineRecordType::RequestAnimationFrame, true, frame);
index 7158de5..30fc979 100644 (file)
@@ -159,6 +159,7 @@ private:
     void toggleCPUInstrument(InstrumentState);
     void toggleMemoryInstrument(InstrumentState);
     void toggleTimelineInstrument(InstrumentState);
+    void toggleAnimationInstrument(InstrumentState);
     void disableBreakpoints();
     void enableBreakpoints();
 
index 6c7019d..1d81b42 100644 (file)
@@ -1,5 +1,220 @@
 2019-11-01  Devin Rousso  <drousso@apple.com>
 
+        Web Inspector: Timelines: add a timeline that shows information about any recorded CSS animation/transition
+        https://bugs.webkit.org/show_bug.cgi?id=203651
+        <rdar://problem/56128726>
+
+        Reviewed by Brian Burg.
+
+        Unlike all other forms of Web Animations, CSS animations/transitions, are _not_ created by
+        JavaScript, and therefore can seemingly appear out of nowhere. This patch expands the Media
+        timeline to be the Media & Animations timeline, which tracks when CSS animations/transitions
+        are created, started, delayed, iterated, canceled, or finished.
+
+        * UserInterface/Protocol/AnimationObserver.js: Added.
+        (WI.AnimationObserver.prototype.trackingStart):
+        (WI.AnimationObserver.prototype.trackingUpdate):
+        (WI.AnimationObserver.prototype.trackingComplete):
+        * UserInterface/Protocol/Target.js:
+        (WI.Target.prototype.get AnimationAgent): Added.
+        * UserInterface/Main.html:
+        * UserInterface/Base/Main.js:
+        (WI.loaded):
+        * UserInterface/Test.html:
+        * UserInterface/Test/Test.js:
+        (WI.loaded):
+        Add an Animation domain for handling the tracking of CSS Web Animations.
+
+        * UserInterface/Models/MediaInstrument.js:
+        (WI.MediaInstrument.prototype.startInstrumentation):
+        (WI.MediaInstrument.prototype.stopInstrumentation):
+        (WI.MediaInstrument):
+        * UserInterface/Models/MediaTimeline.js: Added.
+        (WI.MediaTimeline.prototype.recordForTrackingAnimationId):
+        (WI.MediaTimeline.prototype.recordForMediaElementEvents):
+        (WI.MediaTimeline.prototype.reset):
+        (WI.MediaTimeline.prototype.addRecord):
+        * UserInterface/Models/MediaTimelineRecord.js:
+        (WI.MediaTimelineRecord):
+        (WI.MediaTimelineRecord.async fromJSON):
+        (WI.MediaTimelineRecord.prototype.toJSON):
+        (WI.MediaTimelineRecord.prototype.get trackingAnimationId): Added.
+        (WI.MediaTimelineRecord.prototype.get timestamps): Added.
+        (WI.MediaTimelineRecord.prototype.get activeStartTime): Added.
+        (WI.MediaTimelineRecord.prototype.get updatesDynamically): Added.
+        (WI.MediaTimelineRecord.prototype.get usesActiveStartTime): Added.
+        (WI.MediaTimelineRecord.prototype.get displayName):
+        (WI.MediaTimelineRecord.prototype.get subtitle): Added.
+        (WI.MediaTimelineRecord.prototype.saveIdentityToCookie):
+        (WI.MediaTimelineRecord.prototype.updateProgress): Added.
+        (WI.MediaTimelineRecord.prototype.addDOMEvent): Added.
+        (WI.MediaTimelineRecord.prototype.powerEfficientPlaybackStateChanged): Added.
+        (WI.MediaTimelineRecord.prototype._updateTimes): Added.
+        (WI.MediaTimelineRecord.fromJSON): Deleted.
+        (WI.MediaTimelineRecord.prototype.get domEvent): Deleted.
+        (WI.MediaTimelineRecord.prototype.get isPowerEfficient): Deleted.
+        * UserInterface/Models/Timeline.js:
+        (WI.Timeline.create):
+        * UserInterface/Controllers/TimelineManager.js:
+        (WI.TimelineManager):
+        (WI.TimelineManager.prototype.async processJSON):
+        (WI.TimelineManager.prototype.animationTrackingStarted): Added.
+        (WI.TimelineManager.prototype.animationTrackingUpdated): Added.
+        (WI.TimelineManager.prototype.animationTrackingCompleted): Added.
+        (WI.TimelineManager.prototype._updateAutoCaptureInstruments):
+        (WI.TimelineManager.prototype._handleDOMNodeDidFireEvent):
+        (WI.TimelineManager.prototype._handleDOMNodePowerEfficientPlaybackStateChanged):
+        Start/Stop tracking animations based on whether the Media & Animations timeline is enabled.
+
+        * UserInterface/Views/MediaTimelineOverviewGraph.js:
+        (WI.MediaTimelineOverviewGraph):
+        (WI.MediaTimelineOverviewGraph.get maximumRowCount): Added.
+        (WI.MediaTimelineOverviewGraph.prototype.reset):
+        (WI.MediaTimelineOverviewGraph.prototype.layout):
+        (WI.MediaTimelineOverviewGraph.prototype.updateSelectedRecord):
+        (WI.MediaTimelineOverviewGraph.prototype._processRecord): Added.
+        (WI.MediaTimelineOverviewGraph.prototype._processRecord.compareByStartTime): Added.
+        (WI.MediaTimelineOverviewGraph.prototype._handleRecordAdded):
+        (WI.MediaTimelineOverviewGraph.prototype._handleTimesUpdated): Added.
+        (WI.MediaTimelineOverviewGraph.prototype.shown): Deleted.
+        (WI.MediaTimelineOverviewGraph.prototype.hidden): Deleted.
+        * UserInterface/Views/MediaTimelineOverviewGraph.css:
+        (.timeline-overview-graph.media): Added.
+        (.timeline-overview-graph.media > .graph-row): Added.
+        (.timeline-overview-graph.media > .graph-row > .timeline-record-bar): Added.
+        (.timeline-overview-graph.media > .graph-row > .timeline-record-bar:not(.unfinished) > .segment:not(.inactive)): Added.
+        (.timeline-overview-graph.media:nth-child(even) > .graph-row > .timeline-record-bar:not(.unfinished) > .segment:not(.inactive)): Added.
+        (.timeline-overview-graph.media > .timeline-record-bar): Deleted.
+        (.timeline-overview-graph.media > .timeline-record-bar > .segment): Deleted.
+        * UserInterface/Views/MediaTimelineView.js:
+        (WI.MediaTimelineView):
+        (WI.MediaTimelineView.prototype.shown): Added.
+        (WI.MediaTimelineView.prototype.hidden): Added.
+        (WI.MediaTimelineView.prototype.closed):
+        (WI.MediaTimelineView.prototype.reset):
+        (WI.MediaTimelineView.prototype.dataGridSortComparator):
+        (WI.MediaTimelineView.prototype.dataGridSortComparator.compareDOMNodes):
+        (WI.MediaTimelineView.prototype._processPendingRecords):
+        * UserInterface/Views/MediaTimelineDataGridNode.js:
+        (WI.MediaTimelineDataGridNode):
+        (WI.MediaTimelineDataGridNode.prototype.get data):
+        (WI.MediaTimelineDataGridNode.prototype.createCellContent):
+        (WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren): Added.
+        (WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addReadySegment): ADded.
+        (WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addDelaySegment): ADded.
+        (WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addActiveSegment): ADded.
+        (WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addFullScreenSegment): ADded.
+        (WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addPowerEfficientPlaybackSegment): ADded.
+        (WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addPausedSegment): ADded.
+        (WI.MediaTimelineDataGridNode.prototype.timelineRecordBarCustomChildren.addPlayingSegment): ADded.
+        (WI.MediaTimelineDataGridNode.prototype.filterableDataForColumn):
+        (WI.MediaTimelineDataGridNode.prototype._createNameCellDocumentFragment): Added.
+        (WI.MediaTimelineDataGridNode.prototype.iconClassNames): Deleted.
+
+         * UserInterface/Views/TimelineRecordBar.js:
+        (WI.TimelineRecordBar):
+        (WI.TimelineRecordBar.prototype.refresh):
+        (WI.TimelineRecordBar.prototype._handleClick):
+        * UserInterface/Views/TimelineRecordBar.css:
+        (.timeline-record-bar):
+        (.timeline-record-bar > :matches(img, .segment)):
+        (.timeline-record-bar > img):
+        (.timeline-record-bar > .segment):
+        (body[dir=ltr] .timeline-record-bar > .segment):
+        (body[dir=ltr] .timeline-record-bar > .segment:first-of-type):
+        (body[dir=ltr] .timeline-record-bar > .segment:last-of-type):
+        (body[dir=rtl] .timeline-record-bar > .segment):
+        (body[dir=rtl] .timeline-record-bar > .segment:first-of-type):
+        (body[dir=rtl] .timeline-record-bar > .segment:last-of-type):
+        (.timeline-record-bar > .segment:not(:last-of-type)):
+        (.timeline-record-bar.selected > .segment):
+        (.timeline-record-bar > .segment.inactive,):
+        (.timeline-record-bar.has-inactive-segment > .segment:not(.inactive)):
+        (:focus .selected .timeline-record-bar:not(.has-custom-children) > .segment):
+        (:focus .selected .timeline-record-bar:not(.has-custom-children) > .segment.inactive):
+        (:focus .selected .timeline-record-bar.has-inactive-segment > .segment:not(.inactive)):
+        (.timeline-record-bar.timeline-record-type-network > .segment):
+        (.timeline-record-bar.timeline-record-type-network > .segment.inactive):
+        (.timeline-record-bar.timeline-record-type-layout > .segment):
+        (.timeline-record-bar.timeline-record-type-layout.paint > .segment,):
+        (.timeline-record-bar.timeline-record-type-script > .segment):
+        (.timeline-record-bar.timeline-record-type-script.garbage-collected > .segment,):
+        (.timeline-record-bar.timeline-record-type-media > .segment):
+        (.timeline-record-bar.has-custom-children.timeline-record-type-media > .segment):
+        (.timeline-record-bar.has-custom-children.timeline-record-type-media > .segment.css-animation-ready):
+        (.timeline-record-bar.has-custom-children.timeline-record-type-media > .segment:matches(.css-animation-delay, .media-element-paused)):
+        (.timeline-record-bar.has-custom-children.timeline-record-type-media.media-element > .segment):
+        (.timeline-record-bar.has-custom-children.timeline-record-type-media.media-element > .segment.media-element-full-screen):
+        (.timeline-record-bar.has-custom-children.timeline-record-type-media.media-element > .segment.media-element-power-efficient-playback):
+        (body[dir=ltr] .timeline-record-bar > .segment.inactive,): Deleted.
+        (body[dir=ltr] .timeline-record-bar.has-inactive-segment > .segment:not(.inactive),): Deleted.
+        (:focus .selected .timeline-record-bar > .segment): Deleted.
+        (:focus .selected .timeline-record-bar > .segment.inactive): Deleted.
+        (body[dir=ltr] :focus .selected .timeline-record-bar.has-inactive-segment > .segment:not(.inactive)): Deleted.
+        (body[dir=rtl] :focus .selected .timeline-record-bar.has-inactive-segment > .segment:not(.inactive)): Deleted.
+        Allow timeline record bars to be customized through a delegate callback. If provided, it
+        will be used instead of any default content. It is expected to return an array of objects,
+        each having a `startTime` number, `classNames` array, and `title` string. It can also have a
+        `endTime` number and an `image` string. If `endTime` is `NaN`, the record is unfinished. If
+        `image` is provided, an `<img>` will be used instead of a segment, allowing for markers.
+
+        * UserInterface/Views/TimelineDataGridNode.js:
+        (WI.TimelineDataGridNode.prototype.createCellContent):
+        Add a default fallback for `WI.DOMNode` values.
+
+        * UserInterface/Views/TimelineTabContentView.js:
+        (WI.TimelineTabContentView.displayNameForTimelineType):
+        (WI.TimelineTabContentView.iconClassNameForRecord):
+        (WI.TimelineTabContentView.displayNameForRecord):
+        * UserInterface/Views/TimelineRecordTreeElement.js:
+        * UserInterface/Views/TimelineIcons.css:
+        (.animation-frame-record .icon):
+        (.css-animation-record .icon): Added.
+        (.css-transition-record .icon): Added.
+        (.media-element-record .icon): Added.
+        (.animation-record .icon): Deleted.
+        (.dom-event-record .icon): Deleted.
+        (.dom-event-record.fullscreen .icon): Deleted.
+        (.power-efficient-playback-state-changed-record .icon): Deleted.
+        * UserInterface/Images/DOMEventFullscreen.svg: Removed.
+        * UserInterface/Images/EventCancel.svg: Added.
+        * UserInterface/Images/EventIteration.svg: Added.
+        * UserInterface/Images/EventPause.svg:
+        * UserInterface/Images/EventPlay.svg:
+        * UserInterface/Images/EventProcessing.svg:
+        * UserInterface/Images/EventStop.svg:
+        * UserInterface/Images/PowerEfficientPlaybackStateChanged.svg: Removed.
+        * UserInterface/Images/TimelineRecordAnimationFrame.svg: Renamed from Source/WebInspectorUI/UserInterface/Images/TimelineRecordAnimation.svg.
+        * UserInterface/Images/TimelineRecordCSSAnimation.svg: Added.
+        * UserInterface/Images/TimelineRecordCSSTransition.svg: Added.
+        * UserInterface/Images/TimelineRecordMediaElement.svg: Added.
+        Add new media icons.
+
+        * UserInterface/Models/TimelineRecording.js:
+        (WI.TimelineRecording.async import): Added.
+        (WI.TimelineRecording.import): Deleted.
+        * UserInterface/Models/TimelineRecord.js:
+        * UserInterface/Models/CPUTimelineRecord.js:
+        * UserInterface/Models/HeapAllocationsTimelineRecord.js:
+        (WI.HeapAllocationsTimelineRecord.async fromJSON): Added.
+        (WI.HeapAllocationsTimelineRecord.fromJSON): Deleted.
+        * UserInterface/Models/LayoutTimelineRecord.js:
+        * UserInterface/Models/MemoryTimelineRecord.js:
+        * UserInterface/Models/RenderingFrameTimelineRecord.js:
+        * UserInterface/Models/ResourceTimelineRecord.js:
+        * UserInterface/Models/ScriptTimelineRecord.js:
+        Make the importing of timeline records `async` so we can attempt to rehydrate the DOM nodes
+        of any media records (as well as wait for heap snapshots).
+
+        * UserInterface/Models/DOMNode.js:
+        (WI.DOMNode):
+        (WI.DOMNode.prototype.isMediaElement): Added.
+        (WI.DOMNode.prototype._shouldListenForEventListeners): Deleted.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
+2019-11-01  Devin Rousso  <drousso@apple.com>
+
         Web Inspector: default to focusing the console prompt if no other content is focused after opening Web Inspector
         https://bugs.webkit.org/show_bug.cgi?id=203743
 
index 48b5313..d1bc7a8 100644 (file)
@@ -81,6 +81,8 @@ localizedStrings["1 match"] = "1 match";
 localizedStrings["2D"] = "2D";
 localizedStrings["Accessibility"] = "Accessibility";
 localizedStrings["Action"] = "Action";
+/* Tooltip for a time range bar that represents when a CSS animation/transition is running */
+localizedStrings["Active"] = "Active";
 localizedStrings["Activity Viewer"] = "Activity Viewer";
 localizedStrings["Add"] = "Add";
 localizedStrings["Add %s Rule"] = "Add %s Rule";
@@ -195,8 +197,10 @@ localizedStrings["Bytes Sent"] = "Bytes Sent";
 localizedStrings["CPU"] = "CPU";
 localizedStrings["CPU Usage"] = "CPU Usage";
 localizedStrings["CSP Hash"] = "CSP Hash";
+localizedStrings["CSS Animation"] = "CSS Animation";
 localizedStrings["CSS Canvas"] = "CSS Canvas";
 localizedStrings["CSS Changes:"] = "CSS Changes:";
+localizedStrings["CSS Transition"] = "CSS Transition";
 localizedStrings["CSS canvas \u201C%s\u201D"] = "CSS canvas \u201C%s\u201D";
 localizedStrings["Cached"] = "Cached";
 localizedStrings["Call Frames Truncated"] = "Call Frames Truncated";
@@ -206,6 +210,8 @@ localizedStrings["Call Trees"] = "Call Trees";
 localizedStrings["Calls"] = "Calls";
 localizedStrings["Cancel Automatic Continue"] = "Cancel Automatic Continue";
 localizedStrings["Cancel comparison"] = "Cancel comparison";
+/* Tooltip for a timestamp marker that represents when a CSS animation/transition is canceled */
+localizedStrings["Canceled"] = "Canceled";
 localizedStrings["Canvas"] = "Canvas";
 localizedStrings["Canvas %d"] = "Canvas %d";
 localizedStrings["Canvas %s"] = "Canvas %s";
@@ -321,6 +327,7 @@ localizedStrings["Custom"] = "Custom";
 localizedStrings["DNS"] = "DNS";
 localizedStrings["DOM"] = "DOM";
 localizedStrings["DOM Content Loaded \u2014 %s"] = "DOM Content Loaded \u2014 %s";
+localizedStrings["DOM Event \u201C%s\u201D"] = "DOM Event \u201C%s\u201D";
 localizedStrings["DOM Events"] = "DOM Events";
 localizedStrings["DOM Nodes:"] = "DOM Nodes:";
 localizedStrings["Damping"] = "Damping";
@@ -341,6 +348,8 @@ localizedStrings["Debugs"] = "Debugs";
 localizedStrings["Decoded"] = "Decoded";
 localizedStrings["Default"] = "Default";
 localizedStrings["Deferred pause from blackboxed script"] = "Deferred pause from blackboxed script";
+/* Tooltip for a time range bar that represents when a CSS animation/transition is delayed */
+localizedStrings["Delay"] = "Delay";
 localizedStrings["Delete"] = "Delete";
 localizedStrings["Delete Breakpoint"] = "Delete Breakpoint";
 localizedStrings["Delete Breakpoints"] = "Delete Breakpoints";
@@ -466,7 +475,6 @@ localizedStrings["Ensure that only one banner is used on the page."] = "Ensure t
 localizedStrings["Ensure that only one live region is used on the page."] = "Ensure that only one live region is used on the page.";
 localizedStrings["Ensure that only one main content section is used on the page."] = "Ensure that only one main content section is used on the page.";
 localizedStrings["Ensure that values for \u0022%s\u0022 are valid."] = "Ensure that values for \u0022%s\u0022 are valid.";
-localizedStrings["Entered Full-Screen Mode"] = "Entered Full-Screen Mode";
 localizedStrings["Entire Recording"] = "Entire Recording";
 localizedStrings["Error"] = "Error";
 localizedStrings["Error: "] = "Error: ";
@@ -486,7 +494,6 @@ localizedStrings["Events:"] = "Events:";
 localizedStrings["Example: \u201C%s\u201D"] = "Example: \u201C%s\u201D";
 localizedStrings["Exception with thrown value: %s"] = "Exception with thrown value: %s";
 localizedStrings["Execution context for %s"] = "Execution context for %s";
-localizedStrings["Exited Full-Screen Mode"] = "Exited Full-Screen Mode";
 localizedStrings["Expand All"] = "Expand All";
 localizedStrings["Expand columns"] = "Expand columns";
 localizedStrings["Expanded"] = "Expanded";
@@ -635,6 +642,8 @@ localizedStrings["Inverted"] = "Inverted";
 localizedStrings["Invisible characters"] = "Invisible characters";
 localizedStrings["Invoke getter"] = "Invoke getter";
 localizedStrings["It is evaluated immediately after the global object is created, before any other content has loaded."] = "It is evaluated immediately after the global object is created, before any other content has loaded.";
+/* Tooltip for a timestamp marker that represents when a CSS animation/transition iterates */
+localizedStrings["Iteration"] = "Iteration";
 localizedStrings["JP2"] = "JP2";
 localizedStrings["JPEG"] = "JPEG";
 localizedStrings["JavaScript"] = "JavaScript";
@@ -701,6 +710,8 @@ localizedStrings["Maximum CPU Usage: %s"] = "Maximum CPU Usage: %s";
 localizedStrings["Maximum Size: %s"] = "Maximum Size: %s";
 localizedStrings["Maximum maximum memory size in this recording"] = "Maximum maximum memory size in this recording";
 localizedStrings["Media"] = "Media";
+localizedStrings["Media & Animations"] = "Media & Animations";
+localizedStrings["Media Element"] = "Media Element";
 localizedStrings["Media Event"] = "Media Event";
 localizedStrings["Media Logging:"] = "Media Logging:";
 localizedStrings["MediaSource"] = "MediaSource";
@@ -817,6 +828,8 @@ localizedStrings["Path"] = "Path";
 localizedStrings["Pause Processing"] = "Pause Processing";
 localizedStrings["Pause Reason"] = "Pause Reason";
 localizedStrings["Pause script execution (%s or %s)"] = "Pause script execution (%s or %s)";
+/* Tooltip for a time range bar that represents when the playback of a audio/video element is paused */
+localizedStrings["Paused"] = "Paused";
 /* The number of tests that passed expressed as a percentage, followed by a literal %. */
 localizedStrings["Percentage (of audits)"] = "%s%%";
 localizedStrings["Periods of high CPU utilization will rapidly drain battery. Strive to keep idle pages under %s average CPU utilization."] = "Periods of high CPU utilization will rapidly drain battery. Strive to keep idle pages under %s average CPU utilization.";
@@ -824,12 +837,12 @@ localizedStrings["Ping"] = "Ping";
 localizedStrings["Ping Frame"] = "Ping Frame";
 localizedStrings["Pings"] = "Pings";
 localizedStrings["Play Sound"] = "Play Sound";
+/* Tooltip for a time range bar that represents when the playback of a audio/video element is running */
+localizedStrings["Playing"] = "Playing";
 localizedStrings["Polite"] = "Polite";
 localizedStrings["Pong Frame"] = "Pong Frame";
 localizedStrings["Port"] = "Port";
 localizedStrings["Power Efficient Playback"] = "Power Efficient Playback";
-localizedStrings["Power Efficient Playback Started"] = "Power Efficient Playback Started";
-localizedStrings["Power Efficient Playback Stopped"] = "Power Efficient Playback Stopped";
 localizedStrings["Prefer Shorthands"] = "Prefer Shorthands";
 localizedStrings["Prefer indent using:"] = "Prefer indent using:";
 localizedStrings["Preserve Log"] = "Preserve Log";
@@ -861,6 +874,8 @@ localizedStrings["Queued"] = "Queued";
 localizedStrings["Radial Gradient"] = "Radial Gradient";
 localizedStrings["Range Issue"] = "Range Issue";
 localizedStrings["Readonly"] = "Readonly";
+/* Tooltip for a time range bar that represents when a CSS animation/transition exists but has not started processing */
+localizedStrings["Ready"] = "Ready";
 localizedStrings["Reasons for compositing"] = "Reasons for compositing";
 localizedStrings["Reasons for compositing:"] = "Reasons for compositing:";
 localizedStrings["Record first %s frame"] = "Record first %s frame";
index 4b7fb11..03228be 100644 (file)
@@ -51,6 +51,8 @@ WI.LayoutDirection = {
 WI.loaded = function()
 {
     // Register observers for events from the InspectorBackend.
+    if (InspectorBackend.registerAnimationDispatcher)
+        InspectorBackend.registerAnimationDispatcher(WI.AnimationObserver);
     if (InspectorBackend.registerApplicationCacheDispatcher)
         InspectorBackend.registerApplicationCacheDispatcher(WI.ApplicationCacheObserver);
     if (InspectorBackend.registerCPUProfilerDispatcher)
index d8672de..7633de1 100644 (file)
@@ -347,7 +347,7 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         let overviewData = json.overview;
 
         let identifier = this._nextRecordingIdentifier++;
-        let newRecording = WI.TimelineRecording.import(identifier, recordingData, filename);
+        let newRecording = await WI.TimelineRecording.import(identifier, recordingData, filename);
         this._recordings.push(newRecording);
 
         this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingCreated, {recording: newRecording});
@@ -744,6 +744,58 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         this.capturingStopped();
     }
 
+    // AnimationObserver
+
+    animationTrackingStarted(timestamp)
+    {
+        this.capturingStarted(timestamp);
+    }
+
+    animationTrackingUpdated(timestamp, event)
+    {
+        if (!this._enabled)
+            return;
+
+        console.assert(this.isCapturing());
+        if (!this.isCapturing())
+            return;
+
+        let mediaTimeline = this._activeRecording.timelineForRecordType(WI.TimelineRecord.Type.Media);
+        console.assert(mediaTimeline);
+
+        let record = mediaTimeline.recordForTrackingAnimationId(event.trackingAnimationId);
+        if (!record) {
+            let details = {
+                trackingAnimationId: event.trackingAnimationId,
+            };
+
+            let eventType;
+            if (event.animationName) {
+                eventType = WI.MediaTimelineRecord.EventType.CSSAnimation;
+                details.animationName = event.animationName;
+            } else if (event.transitionProperty) {
+                eventType = WI.MediaTimelineRecord.EventType.CSSTransition;
+                details.transitionProperty = event.transitionProperty;
+            } else {
+                WI.reportInternalError(`Unknown event type for event '${JSON.stringify(event)}'`);
+                return;
+            }
+
+            let domNode = WI.domManager.nodeForId(event.nodeId) || null;
+            console.assert(domNode);
+
+            record = new WI.MediaTimelineRecord(eventType, domNode, details);
+            this._addRecord(record);
+        }
+
+        record.updateAnimationState(timestamp, event.animationState);
+    }
+
+    animationTrackingCompleted(timestamp)
+    {
+        this.capturingStopped(timestamp);
+    }
+
     // Private
 
     _updateCapturingState(state, data = {})
@@ -1308,7 +1360,6 @@ WI.TimelineManager = class TimelineManager extends WI.Object
                 case WI.TimelineRecord.Type.Network:
                 case WI.TimelineRecord.Type.RenderingFrame:
                 case WI.TimelineRecord.Type.Layout:
-                case WI.TimelineRecord.Type.Media:
                     instrumentSet.add(InspectorBackend.Enum.Timeline.Instrument.Timeline);
                     break;
                 case WI.TimelineRecord.Type.CPU:
@@ -1317,6 +1368,11 @@ WI.TimelineManager = class TimelineManager extends WI.Object
                 case WI.TimelineRecord.Type.Memory:
                     instrumentSet.add(InspectorBackend.Enum.Timeline.Instrument.Memory);
                     break;
+                case WI.TimelineRecord.Type.Media:
+                    // COMPATIBILITY (iOS 13): Animation domain did not exist yet.
+                    if (InspectorBackend.hasDomain("Animation"))
+                        instrumentSet.add(InspectorBackend.Enum.Timeline.Instrument.Animation);
+                    break;
                 }
             }
 
@@ -1329,12 +1385,22 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         if (!this._enabled)
             return;
 
+        let domNode = event.target;
+        if (!domNode.isMediaElement())
+            return;
+
         let {domEvent} = event.data;
 
-        this._addRecord(new WI.MediaTimelineRecord(WI.MediaTimelineRecord.EventType.DOMEvent, domEvent.timestamp, {
-            domNode: event.target,
-            domEvent,
-        }));
+        let mediaTimeline = this._activeRecording.timelineForRecordType(WI.TimelineRecord.Type.Media);
+        console.assert(mediaTimeline);
+
+        let record = mediaTimeline.recordForMediaElementEvents(domNode);
+        if (!record) {
+            record = new WI.MediaTimelineRecord(WI.MediaTimelineRecord.EventType.MediaElement, domNode);
+            this._addRecord(record);
+        }
+
+        record.addDOMEvent(domEvent.timestamp, domEvent);
     }
 
     _handleDOMNodePowerEfficientPlaybackStateChanged(event)
@@ -1342,12 +1408,21 @@ WI.TimelineManager = class TimelineManager extends WI.Object
         if (!this._enabled)
             return;
 
+        let domNode = event.target;
+        console.assert(domNode.isMediaElement());
+
         let {timestamp, isPowerEfficient} = event.data;
 
-        this._addRecord(new WI.MediaTimelineRecord(WI.MediaTimelineRecord.EventType.PowerEfficientPlaybackStateChanged, timestamp, {
-            domNode: event.target,
-            isPowerEfficient,
-        }));
+        let mediaTimeline = this._activeRecording.timelineForRecordType(WI.TimelineRecord.Type.Media);
+        console.assert(mediaTimeline);
+
+        let record = mediaTimeline.recordForMediaElementEvents(domNode);
+        if (!record) {
+            record = new WI.MediaTimelineRecord(WI.MediaTimelineRecord.EventType.MediaElement, domNode);
+            this._addRecord(record);
+        }
+
+        record.powerEfficientPlaybackStateChanged(timestamp, isPowerEfficient);
     }
 };
 
diff --git a/Source/WebInspectorUI/UserInterface/Images/DOMEventFullscreen.svg b/Source/WebInspectorUI/UserInterface/Images/DOMEventFullscreen.svg
deleted file mode 100644 (file)
index 66386e7..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
-<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
-    <path fill="rgb(148, 190, 164)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 Z"/>
-    <path fill="rgb(101, 161, 134)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 M 13 2 C 13.550781 2 14 2.449219 14 3 L 14 13 C 14 13.550781 13.550781 14 13 14 L 3 14 C 2.449219 14 2 13.550781 2 13 L 2 3 C 2 2.449219 2.449219 2 3 2 L 13 2"/>
-    <path fill="rgb(101, 161, 134)" d="M 12 3 L 11 3 L 5 3 L 4 3 L 4 4 L 4 12 L 4 12.962999 L 4.962 12.999 L 6.962 13.075 L 8 13.115 L 8 12.076 L 8 9 L 10 9 L 11 9 L 11 8 L 11 7 L 11 6 L 10 6 L 8 6 L 11 6 L 12 6 L 12 5 L 12 4 L 12 3 Z M 11 4 L 11 5 L 7 5 L 7 7 L 10 7 L 10 8 L 7 8 L 7 12.076 L 5 12 L 5 4 L 11 4 Z"/>
-    <path fill="white" d="M 5 12 L 5 4 L 11 4 L 11 5 L 7 5 L 7 7 L 10 7 L 10 8 L 7 8 L 7 12.076 L 5 12 Z"/>
-</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/EventCancel.svg b/Source/WebInspectorUI/UserInterface/Images/EventCancel.svg
new file mode 100644 (file)
index 0000000..3848f7a
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2019 Apple Inc. All rights reserved. -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 14 14">
+    <circle fill="rgb(191, 222, 255)" stroke="rgb(0, 122, 255)" cx="7" cy="7" r="6.25"/>
+    <path fill="white" d="M 7 0.5 c 3.584 0 6.5 2.916 6.5 6.5 s -2.916 6.5 -6.5 6.5 S 0.5 10.584 0.5 7 3.416 0.5 7 0.5 M 7 0 a 7 7 0 1 0 0 14 A 7 7 0 0 0 7 0 z"/>
+    <circle fill="none" stroke="rgb(0, 122, 255)" cx="7" cy="7" r="4"/>
+    <line stroke="rgb(0, 122, 255)" x1="4" y1="4" x2="10" y2="10" />
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/EventIteration.svg b/Source/WebInspectorUI/UserInterface/Images/EventIteration.svg
new file mode 100644 (file)
index 0000000..2352b42
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2019 Apple Inc. All rights reserved. -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 14 14">
+    <circle fill="rgb(191, 222, 255)" stroke="rgb(0, 122, 255)" cx="7" cy="7" r="6.25"/>
+    <path fill="white" d="M 7 0.5 c 3.584 0 6.5 2.916 6.5 6.5 s -2.916 6.5 -6.5 6.5 S 0.5 10.584 0.5 7 3.416 0.5 7 0.5 M 7 0 a 7 7 0 1 0 0 14 A 7 7 0 0 0 7 0 z"/>
+    <path fill="rgb(0, 122, 255)" d="M 10.2778182,6.85756083 L 10.2778182 7.18878965 L 10.2727273 7.18878965 C 10.2727273 8.91145673 8.80727273 10.3070527 7 10.3070527 C 5.19272727 10.3070527 3.72727273 8.91145673 3.72727273 7.18878965 C 3.72727273 5.58253773 5.00654545 4.27563905 6.64509091 4.10448106 L 6.64509091 5.84516477 L 9.92290909 3.92292886 L 6.64509091 2 L 6.64509091 3.39490299 C 4.60218182 3.56606098 3 5.19795196 3 7.18878965 C 3 9.29396366 4.79054545 11 7 11 C 9.18836364 11 10.9636364 9.32514629 10.9970909 7.24699723 L 11 7.24699723 L 11 6.85756083 L 10.2778182 6.85756083 Z"></path>
+</svg>
index 752e8a0..a9f8c23 100644 (file)
@@ -1,5 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2019 Apple Inc. All rights reserved. -->
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 14 14">
-    <circle fill="rgb(191, 222, 255)" cx="7" cy="7" r="6.75"/>
+    <circle fill="rgb(191, 222, 255)" stroke="rgb(0, 122, 255)" cx="7" cy="7" r="6.25"/>
     <path fill="white" d="M 7 0.5 c 3.584 0 6.5 2.916 6.5 6.5 s -2.916 6.5 -6.5 6.5 S 0.5 10.584 0.5 7 3.416 0.5 7 0.5 M 7 0 a 7 7 0 1 0 0 14 A 7 7 0 0 0 7 0 z"/>
     <path fill="rgb(0, 122, 255)" d="M 5.8 10 H 4.2 a 0.2 0.2 0 0 1 -0.2 -0.2 V 4.2 c 0 -0.11 0.09 -0.2 0.2 -0.2 h 1.6 c 0.11 0 0.2 0.09 0.2 0.2 v 5.6 a 0.2 0.2 0 0 1 -0.2 0.2 z M 9.8 10 H 8.2 a 0.2 0.2 0 0 1 -0.2 -0.2 V 4.2 c 0 -0.11 0.09 -0.2 0.2 -0.2 h 1.6 c 0.11 0 0.2 0.09 0.2 0.2 v 5.6 a 0.2 0.2 0 0 1 -0.2 0.2 z"/>
 </svg>
index c063ef7..2b9708f 100644 (file)
@@ -1,5 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2019 Apple Inc. All rights reserved. -->
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 14 14">
-    <circle fill="rgb(191, 222, 255)" cx="7" cy="7" r="6.75"/>
+    <circle fill="rgb(191, 222, 255)" stroke="rgb(0, 122, 255)" cx="7" cy="7" r="6.25"/>
     <path fill="white" d="M 7 0.5 c 3.584 0 6.5 2.916 6.5 6.5 s -2.916 6.5 -6.5 6.5 S 0.5 10.584 0.5 7 3.416 0.5 7 0.5 M 7 0 a 7 7 0 1 0 0 14 A 7 7 0 0 0 7 0 z"/>
     <path fill="rgb(0, 122, 255)" d="M 10.704 6.827 L 5.3 3.676 A 0.2 0.2 0 0 0 5 3.848 v 6.304 a 0.2 0.2 0 0 0 0.3 0.173 l 5.404 -3.152 a 0.2 0.2 0 0 0 0 -0.346 z"/>
 </svg>
index decd634..fce265c 100644 (file)
@@ -1,5 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2019 Apple Inc. All rights reserved. -->
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 14 14">
-    <circle fill="rgb(191, 222, 255)" cx="7" cy="7" r="6.75"/>
+    <circle fill="rgb(191, 222, 255)" stroke="rgb(0, 122, 255)" cx="7" cy="7" r="6.25"/>
     <path fill="white" d="M 7 0.5 c 3.584 0 6.5 2.916 6.5 6.5 s -2.916 6.5 -6.5 6.5 S 0.5 10.584 0.5 7 3.416 0.5 7 0.5 M 7 0 a 7 7 0 1 0 0 14 A 7 7 0 0 0 7 0 z"/>
     <path fill="rgb(0, 122, 255)" d="M 7 8.5 a 1.5 1.5 0 0 1 0 -3 1.5 1.5 0 0 1 0 3 z M 3 8.5 a 1.5 1.5 0 0 1 0 -3 1.5 1.5 0 0 1 0 3 z M 11 8.5 a 1.5 1.5 0 0 1 0 -3 1.5 1.5 0 0 1 0 3 z"/>
 </svg>
index cb7e764..356e5c1 100644 (file)
@@ -1,5 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2019 Apple Inc. All rights reserved. -->
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 14 14">
-    <circle fill="rgb(191, 222, 255)" cx="7" cy="7" r="6.75"/>
+    <circle fill="rgb(191, 222, 255)" stroke="rgb(0, 122, 255)" cx="7" cy="7" r="6.25"/>
     <path fill="white" d="M 7 0.5 c 3.584 0 6.5 2.916 6.5 6.5 s -2.916 6.5 -6.5 6.5 S 0.5 10.584 0.5 7 3.416 0.5 7 0.5 M 7 0 a 7 7 0 1 0 0 14 A 7 7 0 0 0 7 0 z"/>
     <path fill="rgb(0, 122, 255)" d="M 9.8 10 H 4.2 a 0.2 0.2 0 0 1 -0.2 -0.2 V 4.2 c 0 -0.11 0.09 -0.2 0.2 -0.2 h 5.6 c 0.11 0 0.2 0.09 0.2 0.2 v 5.6 a 0.2 0.2 0 0 1 -0.2 0.2 z"/>
 </svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/PowerEfficientPlaybackStateChanged.svg b/Source/WebInspectorUI/UserInterface/Images/PowerEfficientPlaybackStateChanged.svg
deleted file mode 100644 (file)
index 793adf0..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright © 2013 Apple Inc. All rights reserved. -->
-<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
-    <path fill="rgb(148, 190, 164)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 Z"/>
-    <path fill="rgb(101, 161, 134)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 M 13 2 C 13.550781 2 14 2.449219 14 3 L 14 13 C 14 13.550781 13.550781 14 13 14 L 3 14 C 2.449219 14 2 13.550781 2 13 L 2 3 C 2 2.449219 2.449219 2 3 2 L 13 2"/>
-    <path fill="white" d="M 6.78125 7.769531 L 7.136719 7.769531 C 7.757812 7.769531 8.246094 7.636719 8.605469 7.371094 C 8.96875 7.105469 9.148438 6.746094 9.148438 6.292969 C 9.148438 5.496094 8.59375 5.097656 7.488281 5.097656 L 6.78125 5.097656 Z M 5 12 L 5 4 L 8.042969 4 C 9.105469 4 9.863281 4.164062 10.316406 4.488281 C 10.773438 4.816406 11 5.359375 11 6.125 C 11 6.988281 10.695312 7.660156 10.085938 8.144531 C 9.480469 8.625 8.628906 8.863281 7.535156 8.863281 L 6.78125 8.863281 L 6.78125 12 Z"/>
-    <path fill="rgb(101, 161, 134)" d="M 7.78125 6.679688 L 7.78125 6.113281 C 8.019531 6.136719 8.117188 6.1875 8.144531 6.203125 C 8.140625 6.210938 8.148438 6.242188 8.148438 6.292969 C 8.148438 6.441406 8.105469 6.496094 8.015625 6.566406 C 7.953125 6.613281 7.875 6.648438 7.78125 6.679688 M 8.042969 3 L 4 3 L 4 13 L 7.78125 13 L 7.78125 9.863281 C 8.988281 9.824219 9.96875 9.507812 10.707031 8.925781 C 11.550781 8.257812 12 7.289062 12 6.125 C 12 4.753906 11.402344 4.035156 10.902344 3.675781 C 10.265625 3.222656 9.332031 3 8.042969 3 M 6.78125 7.769531 L 7.136719 7.769531 C 7.757812 7.769531 8.246094 7.636719 8.605469 7.371094 C 8.96875 7.105469 9.148438 6.746094 9.148438 6.292969 C 9.148438 5.496094 8.59375 5.097656 7.488281 5.097656 L 6.78125 5.097656 L 6.78125 7.769531 M 8.042969 4 C 9.105469 4 9.863281 4.164062 10.316406 4.488281 C 10.773438 4.816406 11 5.359375 11 6.125 C 11 6.988281 10.695312 7.660156 10.085938 8.144531 C 9.480469 8.625 8.628906 8.863281 7.535156 8.863281 L 6.78125 8.863281 L 6.78125 12 L 5 12 L 5 4 L 8.042969 4"/>
-</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/TimelineRecordCSSAnimation.svg b/Source/WebInspectorUI/UserInterface/Images/TimelineRecordCSSAnimation.svg
new file mode 100644 (file)
index 0000000..dff09b7
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2019 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(148, 190, 164)" d="M 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 L 3 1 Z M 3 1"/>
+    <path fill="rgb(101, 161, 134)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 M 13 2 C 13.550781 2 14 2.449219 14 3 L 14 13 C 14 13.550781 13.550781 14 13 14 L 3 14 C 2.449219 14 2 13.550781 2 13 L 2 3 C 2 2.449219 2.449219 2 3 2 L 13 2"/>
+    <path fill="rgb(101, 161, 134)" d="M 2.61147641 12.75 L 5.96029528 12.75 L 6.8114662 10.628418 L 8.70346974 10.628418 L 9.53482191 12.75 L 13.3777814 12.75 L 9.50565303 2.80078125 L 6.48360478 2.80078125 L 2.61147641 12.75 Z M 2.61147641 12.75"/>
+    <path fill="white" d="M 4.07373047 11.75 L 7.16748047 3.80078125 L 8.82177734 3.80078125 L 11.9155273 11.75 L 10.2182617 11.75 L 9.39111328 9.62841797 L 6.10400391 9.62841797 L 5.27685547 11.75 L 4.07373047 11.75 Z M 6.51220703 8.5703125 L 8.98291016 8.5703125 L 7.74755859 5.40136719 L 6.51220703 8.5703125 Z M 6.51220703 8.5703125"/>
+</svg>
diff --git a/Source/WebInspectorUI/UserInterface/Images/TimelineRecordCSSTransition.svg b/Source/WebInspectorUI/UserInterface/Images/TimelineRecordCSSTransition.svg
new file mode 100644 (file)
index 0000000..67cc3f0
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright © 2019 Apple Inc. All rights reserved. -->
+<svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
+    <path fill="rgb(148, 190, 164)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 Z"/>
+    <path fill="rgb(101, 161, 134)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 M 13 2 C 13.550781 2 14 2.449219 14 3 L 14 13 C 14 13.550781 13.550781 14 13 14 L 3 14 C 2.449219 14 2 13.550781 2 13 L 2 3 C 2 2.449219 2.449219 2 3 2 L 13 2"/>
+    <path fill="rgb(101, 161, 134)" d="M 6.980469 12.742188 C 6.429688 12.742188 5.980469 12.292969 5.980469 11.742188 C 5.980469 11.742188 5.980469 7.34375 5.980469 5.882812 C 5.140625 5.882812 4.136719 5.882812 4.136719 5.882812 C 3.582031 5.882812 3.136719 5.4375 3.136719 4.882812 L 3.136719 3.792969 C 3.136719 3.242188 3.582031 2.792969 4.136719 2.792969 L 11.488281 2.792969 C 12.042969 2.792969 12.488281 3.242188 12.488281 3.792969 L 12.488281 4.882812 C 12.488281 5.4375 12.042969 5.882812 11.488281 5.882812 C 11.488281 5.882812 10.484375 5.882812 9.640625 5.882812 C 9.640625 7.34375 9.640625 11.742188 9.640625 11.742188 C 9.640625 12.292969 9.195312 12.742188 8.640625 12.742188 L 6.980469 12.742188"/>
+    <path fill="white" d="M 6.980469 11.742188 L 6.980469 4.882812 L 4.136719 4.882812 L 4.136719 3.792969 L 11.488281 3.792969 L 11.488281 4.882812 L 8.640625 4.882812 L 8.640625 11.742188 Z"/>
+</svg>
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright © 2018 Apple Inc. All rights reserved. -->
+<!-- Copyright © 2019 Apple Inc. All rights reserved. -->
 <svg xmlns="http://www.w3.org/2000/svg" id="root" version="1.1" viewBox="0 0 16 16">
     <path fill="rgb(148, 190, 164)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 Z"/>
     <path fill="rgb(101, 161, 134)" d="M 13 1 L 3 1 C 1.898438 1 1 1.898438 1 3 L 1 13 C 1 14.101562 1.898438 15 3 15 L 13 15 C 14.101562 15 15 14.101562 15 13 L 15 3 C 15 1.898438 14.101562 1 13 1 M 13 2 C 13.550781 2 14 2.449219 14 3 L 14 13 C 14 13.550781 13.550781 14 13 14 L 3 14 C 2.449219 14 2 13.550781 2 13 L 2 3 C 2 2.449219 2.449219 2 3 2 L 13 2"/>
index 0854733..6c67009 100644 (file)
     <script src="Protocol/PageTarget.js"></script>
     <script src="Protocol/WorkerTarget.js"></script>
 
+    <script src="Protocol/AnimationObserver.js"></script>
     <script src="Protocol/ApplicationCacheObserver.js"></script>
     <script src="Protocol/CPUProfilerObserver.js"></script>
     <script src="Protocol/CSSObserver.js"></script>
     <script src="Models/LogObject.js"></script>
     <script src="Models/LoggingChannel.js"></script>
     <script src="Models/MediaInstrument.js"></script>
+    <script src="Models/MediaTimeline.js"></script>
     <script src="Models/MediaTimelineRecord.js"></script>
     <script src="Models/MemoryCategory.js"></script>
     <script src="Models/MemoryInstrument.js"></script>
index 5fd53b5..7bd9845 100644 (file)
@@ -78,7 +78,7 @@ WI.CPUTimelineRecord = class CPUTimelineRecord extends WI.TimelineRecord
 
     // Import / Export
 
-    static fromJSON(json)
+    static async fromJSON(json)
     {
         return new WI.CPUTimelineRecord(json);
     }
index 04fdad9..a27e1d9 100644 (file)
@@ -151,7 +151,7 @@ WI.DOMNode = class DOMNode extends WI.Object
         this._domEvents = [];
         this._powerEfficientPlaybackRanges = [];
 
-        if (this._shouldListenForEventListeners())
+        if (this.isMediaElement())
             WI.DOMNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleDOMNodeDidFireEvent, this);
     }
 
@@ -767,6 +767,12 @@ WI.DOMNode = class DOMNode extends WI.Object
         return !!this.ownerSVGElement;
     }
 
+    isMediaElement()
+    {
+        let lowerCaseName = this.localName() || this.nodeName().toLowerCase();
+        return lowerCaseName === "video" || lowerCaseName === "audio";
+    }
+
     didFireEvent(eventName, timestamp, data)
     {
         // Called from WI.DOMManager.
@@ -824,12 +830,6 @@ WI.DOMNode = class DOMNode extends WI.Object
         this.dispatchEventToListeners(WI.DOMNode.Event.DidFireEvent, {domEvent});
     }
 
-    _shouldListenForEventListeners()
-    {
-        let lowerCaseName = this.localName() || this.nodeName().toLowerCase();
-        return lowerCaseName === "video" || lowerCaseName === "audio";
-    }
-
     _setAttributesPayload(attrs)
     {
         this._attributes = [];
index 111417f..37eb797 100644 (file)
@@ -38,7 +38,7 @@ WI.HeapAllocationsTimelineRecord = class HeapAllocationsTimelineRecord extends W
 
     // Import / Export
 
-    static fromJSON(json)
+    static async fromJSON(json)
     {
         // NOTE: This just goes through and generates a new heap snapshot,
         // it is not perfect but does what we want. It asynchronously creates
@@ -46,14 +46,14 @@ WI.HeapAllocationsTimelineRecord = class HeapAllocationsTimelineRecord extends W
         // recording, which on an import should be the newly imported recording.
         let {timestamp, title, snapshotStringData} = json;
 
-        let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
-        workerProxy.createImportedSnapshot(snapshotStringData, title, ({objectId, snapshot: serializedSnapshot}) => {
-            let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
-            snapshot.snapshotStringData = snapshotStringData;
-            WI.timelineManager.heapSnapshotAdded(timestamp, snapshot);
+        return await new Promise((resolve, reject) => {
+            let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
+            workerProxy.createImportedSnapshot(snapshotStringData, title, ({objectId, snapshot: serializedSnapshot}) => {
+                let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
+                snapshot.snapshotStringData = snapshotStringData;
+                resolve(new WI.HeapAllocationsTimelineRecord(timestamp, snapshot));
+            });
         });
-
-        return null;
     }
 
     toJSON()
index c1d718b..97d9b54 100644 (file)
@@ -63,7 +63,7 @@ WI.LayoutTimelineRecord = class LayoutTimelineRecord extends WI.TimelineRecord
 
     // Import / Export
 
-    static fromJSON(json)
+    static async fromJSON(json)
     {
         let {eventType, startTime, endTime, callFrames, sourceCodeLocation, quad} = json;
         quad = quad ? WI.Quad.fromJSON(quad) : null;
index cfbba93..327de49 100644 (file)
@@ -49,11 +49,27 @@ WI.MediaInstrument = class MediaInstrument extends WI.Instrument
 
     startInstrumentation(initiatedByBackend)
     {
-        // Nothing to do, media instrumentation is always happening.
+        // Audio/Video/Picture instrumentation is always happening.
+
+        if (!initiatedByBackend) {
+            // COMPATIBILITY (iOS 13): Animation domain did not exist yet.
+            if (InspectorBackend.hasDomain("Animation")) {
+                let target = WI.assumingMainTarget();
+                target.AnimationAgent.startTracking();
+            }
+        }
     }
 
     stopInstrumentation(initiatedByBackend)
     {
-        // Nothing to do, media instrumentation is always happening.
+        // Audio/Video/Picture instrumentation is always happening.
+
+        if (!initiatedByBackend) {
+            // COMPATIBILITY (iOS 13): Animation domain did not exist yet.
+            if (InspectorBackend.hasDomain("Animation")) {
+                let target = WI.assumingMainTarget();
+                target.AnimationAgent.stopTracking();
+            }
+        }
     }
 };
diff --git a/Source/WebInspectorUI/UserInterface/Models/MediaTimeline.js b/Source/WebInspectorUI/UserInterface/Models/MediaTimeline.js
new file mode 100644 (file)
index 0000000..ff49953
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.MediaTimeline = class MediaTimeline extends WI.Timeline
+{
+    // Public
+
+    recordForTrackingAnimationId(trackingAnimationId)
+    {
+        return this._trackingAnimationIdRecordMap.get(trackingAnimationId) || null;
+    }
+
+    recordForMediaElementEvents(domNode)
+    {
+        console.assert(domNode.isMediaElement());
+        return this._mediaElementRecordMap.get(domNode) || null;
+    }
+
+    reset(suppressEvents)
+    {
+        this._trackingAnimationIdRecordMap = new Map;
+        this._mediaElementRecordMap = new Map;
+
+        super.reset(suppressEvents);
+    }
+
+    addRecord(record, options = {})
+    {
+        console.assert(record instanceof WI.MediaTimelineRecord);
+
+        if (record.trackingAnimationId) {
+            console.assert(!this._trackingAnimationIdRecordMap.has(record.trackingAnimationId));
+            this._trackingAnimationIdRecordMap.set(record.trackingAnimationId, record);
+        }
+
+        if (record.eventType === WI.MediaTimelineRecord.EventType.MediaElement) {
+            console.assert(!(record.domNode instanceof WI.DOMNode) || record.domNode.isMediaElement());
+            console.assert(!this._mediaElementRecordMap.has(record.domNode));
+            this._mediaElementRecordMap.set(record.domNode, record);
+        }
+
+        super.addRecord(record, options);
+    }
+};
index d534732..f3a075a 100644 (file)
 
 WI.MediaTimelineRecord = class MediaTimelineRecord extends WI.TimelineRecord
 {
-    constructor(eventType, timestamp, {domNode, domEvent, isPowerEfficient} = {})
+    constructor(eventType, domNodeOrInfo, {trackingAnimationId, animationName, transitionProperty} = {})
     {
-        console.assert(Object.values(WI.MediaTimelineRecord.EventType).includes(eventType));
+        console.assert(Object.values(MediaTimelineRecord.EventType).includes(eventType));
+        console.assert(domNodeOrInfo instanceof WI.DOMNode || (!isEmptyObject(domNodeOrInfo) && domNodeOrInfo.displayName && domNodeOrInfo.cssPath));
 
-        super(WI.TimelineRecord.Type.Media, timestamp, timestamp);
+        super(WI.TimelineRecord.Type.Media);
 
         this._eventType = eventType;
-        this._domNode = domNode || null;
-        this._domEvent = domEvent || null;
-        this._isPowerEfficient = isPowerEfficient || false;
+        this._domNode = domNodeOrInfo;
+        this._domNodeDisplayName = domNodeOrInfo.displayName;
+        this._domNodeCSSPath = domNodeOrInfo instanceof WI.DOMNode ? WI.cssPath(domNodeOrInfo, {full: true}) : domNodeOrInfo.cssPath;
+
+        // Web Animation
+        console.assert(trackingAnimationId === undefined || typeof trackingAnimationId === "string");
+        this._trackingAnimationId = trackingAnimationId || null;
+
+        // CSS Web Animation
+        console.assert(animationName === undefined || typeof animationName === "string");
+        console.assert(transitionProperty === undefined || typeof transitionProperty === "string");
+        this._animationName = animationName || null;
+        this._transitionProperty = transitionProperty || null;
+
+        this._timestamps = [];
+        this._activeStartTime = NaN;
     }
 
     // Import / Export
 
-    static fromJSON(json)
+    static async fromJSON(json)
     {
-        let {eventType, timestamp} = json;
+        let {eventType, domNodeDisplayName, domNodeCSSPath, animationName, transitionProperty, timestamps} = json;
+
+        let documentNode = null;
+        if (InspectorBackend.hasDomain("DOM"))
+            documentNode = await new Promise((resolve) => WI.domManager.requestDocument(resolve));
+
+        let domNode = null;
+        if (documentNode && domNodeCSSPath) {
+            try {
+                let nodeId = await WI.domManager.querySelector(documentNode, domNodeCSSPath);
+                if (nodeId)
+                    domNode = WI.domManager.nodeForId(nodeId);
+            } catch { }
+        }
+        if (!domNode) {
+            domNode = {
+                displayName: domNodeDisplayName,
+                cssPath: domNodeCSSPath,
+            };
+        }
+
+        let record = new MediaTimelineRecord(eventType, domNode, {animationName, transitionProperty});
 
-        // COMPATIBILITY (iOS 12.2): isLowPower was renamed to isPowerEfficient.
-        if ("isLowPower" in json && !("isPowerEfficient" in json))
-            json.isPowerEfficient = json.isLowPower;
+        if (Array.isArray(timestamps) && timestamps.length) {
+            record._timestamps = [];
+            for (let item of timestamps) {
+                if (item.type === MediaTimelineRecord.TimestampType.MediaElementDOMEvent) {
+                    if (documentNode && item.originatorCSSPath) {
+                        try {
+                            let nodeId = await WI.domManager.querySelector(documentNode, item.originatorCSSPath);
+                            if (nodeId)
+                                item.originator = WI.domManager.nodeForId(nodeId);
+                        } catch { }
+                        if (!item.originator) {
+                            item.originator = {
+                                displayName: item.originatorDisplayName,
+                                cssPath: item.originatorCSSPath,
+                            };
+                        }
+                    }
+                }
+                record._timestamps.push(item);
+            }
+        }
 
-        return new WI.MediaTimelineRecord(eventType, timestamp, json);
+        return record;
     }
 
     toJSON()
     {
-        // FIXME: DOMNode
+        let json = {
+            eventType: this._eventType,
+            domNodeDisplayName: this._domNodeDisplayName,
+            domNodeCSSPath: this._domNodeCSSPath,
+        };
+
+        if (this._animationName)
+            json.animationName = this._animationName;
 
-        // Don't include the DOMEvent's originator.
-        let domEvent = this._domEvent;
-        if (domEvent && domEvent.originator) {
-            domEvent = Object.shallowCopy(domEvent);
-            domEvent.originator = undefined;
+        if (this._transitionProperty)
+            json.transitionProperty = this._transitionProperty;
+
+        if (this._timestamps.length) {
+            json.timestamps = this._timestamps.map((item) => {
+                if (item.type === MediaTimelineRecord.TimestampType.MediaElementDOMEvent && item.originator instanceof WI.DOMNode)
+                    delete item.originator;
+                return item;
+            });
         }
 
-        return {
-            type: this.type,
-            eventType: this._eventType,
-            timestamp: this.startTime,
-            domEvent,
-            isPowerEfficient: this._isPowerEfficient,
-        };
+        return json;
     }
 
     // Public
 
     get eventType() { return this._eventType; }
     get domNode() { return this._domNode; }
-    get domEvent() { return this._domEvent; }
-    get isPowerEfficient() { return this._isPowerEfficient; }
+    get trackingAnimationId() { return this._trackingAnimationId; }
+    get timestamps() { return this._timestamps; }
+    get activeStartTime() { return this._activeStartTime; }
+
+    get updatesDynamically()
+    {
+        return true;
+    }
+
+    get usesActiveStartTime()
+    {
+        return true;
+    }
 
     get displayName()
     {
-        if (this._eventType === WI.MediaTimelineRecord.EventType.DOMEvent && this._domEvent) {
-            let eventName = this._domEvent.eventName;
-            if (eventName === "webkitfullscreenchange" && this._domEvent.data)
-                return this._domEvent.data.enabled ? WI.UIString("Entered Full-Screen Mode") : WI.UIString("Exited Full-Screen Mode");
-            return eventName;
-        }
+        switch (this._eventType) {
+        case MediaTimelineRecord.EventType.CSSAnimation:
+            return this._animationName;
 
-        if (this._eventType === MediaTimelineRecord.EventType.PowerEfficientPlaybackStateChanged)
-            return this._isPowerEfficient ? WI.UIString("Power Efficient Playback Started") : WI.UIString("Power Efficient Playback Stopped");
+        case MediaTimelineRecord.EventType.CSSTransition:
+            return this._transitionProperty;
 
-        if (this._domNode)
-            return this._domNode.displayName;
+        case MediaTimelineRecord.EventType.MediaElement:
+            return WI.UIString("Media Element");
+        }
 
         console.error("Unknown media record event type: ", this._eventType, this);
         return WI.UIString("Media Event");
     }
 
+    get subtitle()
+    {
+        switch (this._eventType) {
+        case MediaTimelineRecord.EventType.CSSAnimation:
+            return WI.UIString("CSS Animation");
+
+        case MediaTimelineRecord.EventType.CSSTransition:
+            return WI.UIString("CSS Transition");
+        }
+
+        return "";
+    }
+
     saveIdentityToCookie(cookie)
     {
         super.saveIdentityToCookie(cookie);
 
         cookie["media-timeline-record-event-type"] = this._eventType;
-        if (this._eventType === MediaTimelineRecord.EventType.PowerEfficientPlaybackStateChanged)
-            cookie["media-timeline-record-power-efficient-playback"] = this._isPowerEfficient;
-        if (this._domNode)
-            cookie["media-timeline-record-dom-node"] = this._domNode.path();
-        if (this._domEvent) {
-            cookie["media-timeline-record-dom-event"] = this._domEvent.eventName;
-            if (this._domEvent.data && this._domEvent.data.enabled)
-                cookie["media-timeline-record-dom-event-active"] = true;
+        cookie["media-timeline-record-dom-node"] = this._domNode instanceof WI.DOMNode ? this._domNode.path() : this._domNode;
+        if (this._animationName)
+            cookie["media-timeline-record-animation-name"] = this._animationName;
+        if (this._transitionProperty)
+            cookie["media-timeline-record-transition-property"] = this._transitionProperty;
+    }
+
+    // TimelineManager
+
+    updateAnimationState(timestamp, animationState)
+    {
+        console.assert(this._eventType === MediaTimelineRecord.EventType.CSSAnimation || this._eventType === MediaTimelineRecord.EventType.CSSTransition);
+        console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp);
+
+        let type;
+        switch (animationState) {
+        case InspectorBackend.Enum.Animation.AnimationState.Ready:
+            type = MediaTimelineRecord.TimestampType.CSSAnimationReady;
+            break;
+        case InspectorBackend.Enum.Animation.AnimationState.Delayed:
+            type = MediaTimelineRecord.TimestampType.CSSAnimationDelay;
+            break;
+        case InspectorBackend.Enum.Animation.AnimationState.Active:
+            type = MediaTimelineRecord.TimestampType.CSSAnimationActive;
+            break;
+        case InspectorBackend.Enum.Animation.AnimationState.Canceled:
+            type = MediaTimelineRecord.TimestampType.CSSAnimationCancel;
+            break;
+        case InspectorBackend.Enum.Animation.AnimationState.Done:
+            type = MediaTimelineRecord.TimestampType.CSSAnimationDone;
+            break;
+        }
+        console.assert(type);
+
+        this._timestamps.push({type, timestamp});
+
+        this._updateTimes();
+    }
+
+    addDOMEvent(timestamp, domEvent)
+    {
+        console.assert(this._eventType === MediaTimelineRecord.EventType.MediaElement);
+        console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp);
+
+        let data = {
+            type: MediaTimelineRecord.TimestampType.MediaElementDOMEvent,
+            timestamp,
+            eventName: domEvent.eventName,
+        };
+        if (domEvent.originator instanceof WI.DOMNode) {
+            data.originator = domEvent.originator;
+            data.originatorDisplayName = data.originator.displayName;
+            data.originatorCSSPath = WI.cssPath(data.originator, {full: true});
         }
+        if (!isEmptyObject(domEvent.data))
+            data.data = domEvent.data;
+        this._timestamps.push(data);
+
+        this._updateTimes();
+    }
+
+    powerEfficientPlaybackStateChanged(timestamp, isPowerEfficient)
+    {
+        console.assert(this._eventType === MediaTimelineRecord.EventType.MediaElement);
+        console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp);
+
+        this._timestamps.push({
+            type: MediaTimelineRecord.TimestampType.MediaElementPowerEfficientPlaybackStateChange,
+            timestamp,
+            isPowerEfficient,
+        });
+
+        this._updateTimes();
+    }
+
+    // Private
+
+    _updateTimes()
+    {
+        let oldStartTime = this.startTime;
+        let oldEndTime = this.endTime;
+
+        let firstItem = this._timestamps[0];
+        let lastItem = this._timestamps.lastValue;
+
+        if (isNaN(this._startTime))
+            this._startTime = firstItem.timestamp;
+
+        if (isNaN(this._activeStartTime)) {
+            if (this._eventType === MediaTimelineRecord.EventType.MediaElement)
+                this._activeStartTime = firstItem.timestamp;
+            else if (firstItem.type === MediaTimelineRecord.TimestampType.CSSAnimationActive)
+                this._activeStartTime = firstItem.timestamp;
+        }
+
+        switch (lastItem.type) {
+        case MediaTimelineRecord.TimestampType.CSSAnimationCancel:
+        case MediaTimelineRecord.TimestampType.CSSAnimationDone:
+            this._endTime = lastItem.timestamp;
+            break;
+
+        case MediaTimelineRecord.TimestampType.MediaElementDOMEvent:
+            if (WI.DOMNode.isPlayEvent(lastItem.eventName))
+                this._endTime = NaN;
+            else if (!isNaN(this._endTime) || WI.DOMNode.isPauseEvent(lastItem.eventName) || WI.DOMNode.isStopEvent(lastItem.eventName))
+                this._endTime = lastItem.timestamp;
+            break;
+        }
+
+        if (this.startTime !== oldStartTime || this.endTime !== oldEndTime)
+            this.dispatchEventToListeners(WI.TimelineRecord.Event.Updated);
     }
 };
 
 WI.MediaTimelineRecord.EventType = {
-    DOMEvent: "dom-event",
-    PowerEfficientPlaybackStateChanged: "power-efficient-playback-state-changed",
+    CSSAnimation: "css-animation",
+    CSSTransition: "css-transition",
+    MediaElement: "media-element",
+};
+
+WI.MediaTimelineRecord.TimestampType = {
+    CSSAnimationReady: "css-animation-ready",
+    CSSAnimationDelay: "css-animation-delay",
+    CSSAnimationActive: "css-animation-active",
+    CSSAnimationCancel: "css-animation-cancel",
+    CSSAnimationDone: "css-animation-done",
+    // CSS transitions share the same timestamp types.
+
+    MediaElementDOMEvent: "media-element-dom-event",
+    MediaElementPowerEfficientPlaybackStateChange: "media-element-power-efficient-playback-state-change",
 };
index 58a52a7..3229f14 100644 (file)
@@ -88,7 +88,7 @@ WI.MemoryTimelineRecord = class MemoryTimelineRecord extends WI.TimelineRecord
 
     // Import / Export
 
-    static fromJSON(json)
+    static async fromJSON(json)
     {
         let {timestamp, categories} = json;
         return new WI.MemoryTimelineRecord(timestamp, categories);
index cc2e1b2..d1037e5 100644 (file)
@@ -71,7 +71,7 @@ WI.RenderingFrameTimelineRecord = class RenderingFrameTimelineRecord extends WI.
 
     // Import / Export
 
-    static fromJSON(json)
+    static async fromJSON(json)
     {
         let {startTime, endTime} = json;
         let record = new WI.RenderingFrameTimelineRecord(startTime, endTime);
index 098721e..3b8386c 100644 (file)
@@ -35,7 +35,7 @@ WI.ResourceTimelineRecord = class ResourceTimelineRecord extends WI.TimelineReco
 
     // Import / Export
 
-    static fromJSON(json)
+    static async fromJSON(json)
     {
         let {entry, archiveStartTime} = json;
         let localResource = WI.LocalResource.fromHAREntry(entry, archiveStartTime);
index e98fa2b..86d0bf7 100644 (file)
@@ -51,7 +51,7 @@ WI.ScriptTimelineRecord = class ScriptTimelineRecord extends WI.TimelineRecord
 
     // Import / Export
 
-    static fromJSON(json)
+    static async fromJSON(json)
     {
         let {eventType, startTime, endTime, callFrames, sourceCodeLocation, details, profilePayload, extraDetails} = json;
 
index 940719f..6c7141b 100644 (file)
@@ -47,6 +47,9 @@ WI.Timeline = class Timeline extends WI.Object
         if (type === WI.TimelineRecord.Type.Memory)
             return new WI.MemoryTimeline(type);
 
+        if (type === WI.TimelineRecord.Type.Media)
+            return new WI.MediaTimeline(type);
+
         return new WI.Timeline(type);
     }
 
index 288f201..621f507 100644 (file)
@@ -44,7 +44,7 @@ WI.TimelineRecord = class TimelineRecord extends WI.Object
 
     // Import / Export
 
-    static fromJSON(json)
+    static async fromJSON(json)
     {
         switch (json.type) {
         case WI.TimelineRecord.Type.Network:
@@ -90,7 +90,7 @@ WI.TimelineRecord = class TimelineRecord extends WI.Object
     get activeStartTime()
     {
         // Implemented by subclasses if needed.
-        return this._startTime;
+        return this.startTime;
     }
 
     get unadjustedStartTime()
index 1c4f47a..cf54013 100644 (file)
@@ -77,7 +77,7 @@ WI.TimelineRecording = class TimelineRecording extends WI.Object
 
     // Import / Export
 
-    static import(identifier, json, displayName)
+    static async import(identifier, json, displayName)
     {
         let {startTime, endTime, discontinuities, instrumentTypes, records, markers, memoryPressureEvents, sampleStackTraces, sampleDurations} = json;
         let importedDisplayName = WI.UIString("Imported - %s").format(displayName);
@@ -93,7 +93,7 @@ WI.TimelineRecording = class TimelineRecording extends WI.Object
         recording.initializeCallingContextTrees(sampleStackTraces, sampleDurations);
 
         for (let recordJSON of records) {
-            let record = WI.TimelineRecord.fromJSON(recordJSON);
+            let record = await WI.TimelineRecord.fromJSON(recordJSON);
             if (record) {
                 recording.addRecord(record);
 
diff --git a/Source/WebInspectorUI/UserInterface/Protocol/AnimationObserver.js b/Source/WebInspectorUI/UserInterface/Protocol/AnimationObserver.js
new file mode 100644 (file)
index 0000000..b1195ed
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.AnimationObserver = class AnimationObserver extends InspectorBackend.Dispatcher
+{
+    // Events defined by the "Animation" domain.
+
+    trackingStart(timestamp)
+    {
+        WI.timelineManager.animationTrackingStarted(timestamp);
+    }
+
+    trackingUpdate(timestamp, event)
+    {
+        WI.timelineManager.animationTrackingUpdated(timestamp, event);
+    }
+
+    trackingComplete(timestamp)
+    {
+        WI.timelineManager.animationTrackingCompleted(timestamp);
+    }
+};
index e76facb..0a30dff 100644 (file)
@@ -107,8 +107,9 @@ WI.Target = class Target extends WI.Object
 
     // Agents
 
-    get AuditAgent() { return this._agents.Audit; }
+    get AnimationAgent() { return this._agents.Animation; }
     get ApplicationCacheAgent() { return this._agents.ApplicationCache; }
+    get AuditAgent() { return this._agents.Audit; }
     get CPUProfilerAgent() { return this._agents.CPUProfiler; }
     get CSSAgent() { return this._agents.CSS; }
     get CanvasAgent() { return this._agents.Canvas; }
index adca29b..2368221 100644 (file)
@@ -84,6 +84,7 @@
     <script src="Protocol/WorkerTarget.js"></script>
 
     <script src="Protocol/InspectorObserver.js"></script>
+    <script src="Protocol/AnimationObserver.js"></script>
     <script src="Protocol/ApplicationCacheObserver.js"></script>
     <script src="Protocol/CPUProfilerObserver.js"></script>
     <script src="Protocol/CSSObserver.js"></script>
     <script src="Models/LocalResourceOverride.js"></script>
     <script src="Models/LoggingChannel.js"></script>
     <script src="Models/MediaInstrument.js"></script>
+    <script src="Models/MediaTimeline.js"></script>
     <script src="Models/MediaTimelineRecord.js"></script>
     <script src="Models/MemoryCategory.js"></script>
     <script src="Models/MemoryInstrument.js"></script>
index 8eead55..83aa609 100644 (file)
@@ -27,6 +27,7 @@ WI.loaded = function()
 {
     // Register observers for events from the InspectorBackend.
     // The initialization order should match the same in Main.js.
+    InspectorBackend.registerAnimationDispatcher(WI.AnimationObserver);
     InspectorBackend.registerApplicationCacheDispatcher(WI.ApplicationCacheObserver);
     InspectorBackend.registerCPUProfilerDispatcher(WI.CPUProfilerObserver);
     InspectorBackend.registerCSSDispatcher(WI.CSSObserver);
index f323907..d8ce481 100644 (file)
@@ -30,48 +30,39 @@ WI.DOMTreeElementPathComponent = class DOMTreeElementPathComponent extends WI.Hi
         var node = domTreeElement.representedObject;
 
         var title = null;
-        var className = null;
 
         switch (node.nodeType()) {
         case Node.ELEMENT_NODE:
             if (node.isPseudoElement()) {
-                className = WI.DOMTreeElementPathComponent.DOMPseudoElementIconStyleClassName;
                 title = "::" + node.pseudoType();
             } else {
-                className = WI.DOMTreeElementPathComponent.DOMElementIconStyleClassName;
                 title = node.displayName;
             }
             break;
 
         case Node.TEXT_NODE:
-            className = WI.DOMTreeElementPathComponent.DOMTextNodeIconStyleClassName;
             title = "\"" + node.nodeValue().truncateEnd(32) + "\"";
             break;
 
         case Node.COMMENT_NODE:
-            className = WI.DOMTreeElementPathComponent.DOMCommentIconStyleClassName;
             title = "<!--" + node.nodeValue().truncateEnd(32) + "-->";
             break;
 
         case Node.DOCUMENT_TYPE_NODE:
-            className = WI.DOMTreeElementPathComponent.DOMDocumentTypeIconStyleClassName;
             title = "<!DOCTYPE>";
             break;
 
         case Node.DOCUMENT_NODE:
-            className = WI.DOMTreeElementPathComponent.DOMDocumentIconStyleClassName;
             title = node.nodeNameInCorrectCase();
             break;
 
         case Node.CDATA_SECTION_NODE:
-            className = WI.DOMTreeElementPathComponent.DOMCharacterDataIconStyleClassName;
             title = "<![CDATA[" + node.truncateEnd(32) + "]]>";
             break;
 
         case Node.DOCUMENT_FRAGMENT_NODE:
             // FIXME: At some point we might want a different icon for this.
             // <rdar://problem/12800950> Need icon for DOCUMENT_FRAGMENT_NODE and PROCESSING_INSTRUCTION_NODE
-            className = WI.DOMTreeElementPathComponent.DOMDocumentTypeIconStyleClassName;
             if (node.shadowRootType())
                 title = WI.UIString("Shadow Content");
             else
@@ -81,21 +72,57 @@ WI.DOMTreeElementPathComponent = class DOMTreeElementPathComponent extends WI.Hi
         case Node.PROCESSING_INSTRUCTION_NODE:
             // FIXME: At some point we might want a different icon for this.
             // <rdar://problem/12800950> Need icon for DOCUMENT_FRAGMENT_NODE and PROCESSING_INSTRUCTION_NODE.
-            className = WI.DOMTreeElementPathComponent.DOMDocumentTypeIconStyleClassName;
             title = node.nodeNameInCorrectCase();
             break;
 
         default:
             console.error("Unknown DOM node type: ", node.nodeType());
-            className = WI.DOMTreeElementPathComponent.DOMNodeIconStyleClassName;
             title = node.nodeNameInCorrectCase();
         }
 
-        super(title, className, representedObject || domTreeElement.representedObject);
+        super(title, DOMTreeElementPathComponent.iconClassNameForNode(node), representedObject || domTreeElement.representedObject);
 
         this._domTreeElement = domTreeElement;
     }
 
+    static iconClassNameForNode(domNode)
+    {
+        switch (domNode.nodeType()) {
+        case Node.ELEMENT_NODE:
+            if (domNode.isPseudoElement())
+                return WI.DOMTreeElementPathComponent.DOMPseudoElementIconStyleClassName;
+            return WI.DOMTreeElementPathComponent.DOMElementIconStyleClassName;
+
+        case Node.TEXT_NODE:
+            return WI.DOMTreeElementPathComponent.DOMTextNodeIconStyleClassName;
+
+        case Node.COMMENT_NODE:
+            return WI.DOMTreeElementPathComponent.DOMCommentIconStyleClassName;
+
+        case Node.DOCUMENT_TYPE_NODE:
+            return WI.DOMTreeElementPathComponent.DOMDocumentTypeIconStyleClassName;
+
+        case Node.DOCUMENT_NODE:
+            return WI.DOMTreeElementPathComponent.DOMDocumentIconStyleClassName;
+
+        case Node.CDATA_SECTION_NODE:
+            return WI.DOMTreeElementPathComponent.DOMCharacterDataIconStyleClassName;
+
+        case Node.DOCUMENT_FRAGMENT_NODE:
+            // FIXME: At some point we might want a different icon for this.
+            // <rdar://problem/12800950> Need icon for DOCUMENT_FRAGMENT_NODE and PROCESSING_INSTRUCTION_NODE
+            return WI.DOMTreeElementPathComponent.DOMDocumentTypeIconStyleClassName;
+
+        case Node.PROCESSING_INSTRUCTION_NODE:
+            // FIXME: At some point we might want a different icon for this.
+            // <rdar://problem/12800950> Need icon for DOCUMENT_FRAGMENT_NODE and PROCESSING_INSTRUCTION_NODE.
+            return WI.DOMTreeElementPathComponent.DOMDocumentTypeIconStyleClassName;
+        }
+
+        console.error("Unknown DOM node type: ", domNode.nodeType());
+        return WI.DOMTreeElementPathComponent.DOMNodeIconStyleClassName;
+    }
+
     // Public
 
     get domTreeElement()
index 6dceac1..1063f2b 100644 (file)
@@ -41,11 +41,8 @@ WI.MediaTimelineDataGridNode = class MediaTimelineDataGridNode extends WI.Timeli
 
         this._cachedData = super.data;
         this._cachedData.name = this.record.displayName;
-        if (this.record.domNode)
-            this._cachedData.element = this.record.domNode;
-        this._cachedData.time = this.record.startTime - (this.graphDataSource ? this.graphDataSource.zeroTime : 0);
-        if (this.record.eventType === WI.MediaTimelineRecord.EventType.DOMEvent && this.record.domEvent.originator)
-            this._cachedData.originator = this.record.domEvent.originator;
+        this._cachedData.element = this.record.domNode;
+        this._cachedData.source = this.record.domNode; // Timeline Overview
         return this._cachedData;
     }
 
@@ -56,46 +53,297 @@ WI.MediaTimelineDataGridNode = class MediaTimelineDataGridNode extends WI.Timeli
         switch (columnIdentifier) {
         case "name":
             cell.classList.add(...this.iconClassNames());
-            return value;
+            return this._createNameCellDocumentFragment();
 
-        case "source": // Timeline Overview
         case "element":
-            return value ? WI.linkifyNodeReference(value) : emDash;
-
-        case "time": {
-            const higherResolution = true;
-            return Number.secondsToString(value, higherResolution);
-        }
-
-        case "originator":
-            return value ? WI.linkifyNodeReference(value) : zeroWidthSpace;
+        case "source": // Timeline Overview
+            if (!(value instanceof WI.DOMNode)) {
+                cell.classList.add(WI.DOMTreeElementPathComponent.DOMNodeIconStyleClassName);
+                return value.displayName;
+            }
+            break;
         }
 
         return super.createCellContent(columnIdentifier, cell);
     }
 
-    iconClassNames()
+    // TimelineRecordBar delegate
+
+    timelineRecordBarCustomChildren(timelineRecordBar)
     {
-        let iconClassNames = super.iconClassNames();
-        if (this.record.eventType === WI.MediaTimelineRecord.EventType.DOMEvent && this.record.domEvent.eventName === "webkitfullscreenchange")
-            iconClassNames.push("fullscreen");
-        return iconClassNames;
+        let children = [];
+
+        let record = this.record;
+        let timestamps = record.timestamps;
+
+        switch (record.eventType) {
+        case WI.MediaTimelineRecord.EventType.CSSAnimation:
+        case WI.MediaTimelineRecord.EventType.CSSTransition: {
+            let readyStartTime = NaN;
+            function addReadySegment(startTime, endTime) {
+                children.push({
+                    startTime,
+                    endTime,
+                    classNames: ["segment", "css-animation-ready"],
+                    title: WI.UIString("Ready", "Tooltip for a time range bar that represents when a CSS animation/transition exists but has not started processing"),
+                });
+                readyStartTime = NaN;
+            }
+
+            let delayStartTime = NaN;
+            function addDelaySegment(startTime, endTime) {
+                children.push({
+                    startTime,
+                    endTime,
+                    classNames: ["segment", "css-animation-delay"],
+                    title: WI.UIString("Delay", "Tooltip for a time range bar that represents when a CSS animation/transition is delayed"),
+                });
+                delayStartTime = NaN;
+            }
+
+            let activeStartTime = NaN;
+            function addActiveSegment(startTime, endTime) {
+                children.push({
+                    startTime,
+                    endTime,
+                    classNames: ["segment", "css-animation-active"],
+                    title: WI.UIString("Active", "Tooltip for a time range bar that represents when a CSS animation/transition is running"),
+                });
+                activeStartTime = NaN;
+            }
+
+            for (let item of timestamps) {
+                switch (item.type) {
+                case WI.MediaTimelineRecord.TimestampType.CSSAnimationReady:
+                    if (isNaN(readyStartTime))
+                        readyStartTime = item.timestamp;
+                    break;
+                case WI.MediaTimelineRecord.TimestampType.CSSAnimationDelay:
+                    if (isNaN(delayStartTime))
+                        delayStartTime = item.timestamp;
+                    if (!isNaN(readyStartTime))
+                        addReadySegment(readyStartTime, item.timestamp);
+                    break;
+                case WI.MediaTimelineRecord.TimestampType.CSSAnimationActive:
+                    if (isNaN(activeStartTime))
+                        activeStartTime = item.timestamp;
+                    if (!isNaN(readyStartTime))
+                        addReadySegment(readyStartTime, item.timestamp);
+                    if (!isNaN(delayStartTime))
+                        addDelaySegment(delayStartTime, item.timestamp);
+                    break;
+                case WI.MediaTimelineRecord.TimestampType.CSSAnimationCancel:
+                case WI.MediaTimelineRecord.TimestampType.CSSAnimationDone:
+                    if (!isNaN(readyStartTime))
+                        addReadySegment(readyStartTime, item.timestamp);
+                    if (!isNaN(delayStartTime))
+                        addDelaySegment(delayStartTime, item.timestamp);
+                    if (!isNaN(activeStartTime))
+                        addActiveSegment(activeStartTime, item.timestamp);
+                    break;
+                }
+            }
+
+            if (!isNaN(readyStartTime))
+                addReadySegment(readyStartTime, NaN);
+            if (!isNaN(delayStartTime))
+                addDelaySegment(delayStartTime, NaN);
+            if (!isNaN(activeStartTime))
+                addActiveSegment(activeStartTime, NaN);
+
+            break;
+        }
+
+        case WI.MediaTimelineRecord.EventType.MediaElement: {
+            let fullScreenSegments = [];
+            let powerEfficientPlaybackSegments = [];
+            let activeSegments = [];
+
+            let fullScreenStartTime = NaN;
+            let fullScreenOriginator = null;
+            function addFullScreenSegment(startTime, endTime) {
+                fullScreenSegments.push({
+                    startTime,
+                    endTime,
+                    classNames: ["segment", "media-element-full-screen"],
+                    title: fullScreenOriginator ? WI.UIString("Full-Screen from \u201C%s\u201D").format(fullScreenOriginator.displayName) : WI.UIString("Full-Screen"),
+                });
+                fullScreenStartTime = NaN;
+                fullScreenOriginator = null;
+            }
+
+            let powerEfficientPlaybackStartTime = NaN;
+            function addPowerEfficientPlaybackSegment(startTime, endTime) {
+                powerEfficientPlaybackSegments.push({
+                    startTime,
+                    endTime,
+                    classNames: ["segment", "media-element-power-efficient-playback"],
+                    title: WI.UIString("Power Efficient Playback"),
+                });
+                powerEfficientPlaybackStartTime = NaN;
+            }
+
+            let pausedStartTime = NaN;
+            function addPausedSegment(startTime, endTime) {
+                activeSegments.push({
+                    startTime,
+                    endTime,
+                    classNames: ["segment", "media-element-paused"],
+                    title: WI.UIString("Paused", "Tooltip for a time range bar that represents when the playback of a audio/video element is paused"),
+                });
+                pausedStartTime = NaN;
+            }
+
+            let playingStartTime = NaN;
+            function addPlayingSegment(startTime, endTime) {
+                activeSegments.push({
+                    startTime,
+                    endTime,
+                    classNames: ["segment", "media-element-playing"],
+                    title: WI.UIString("Playing", "Tooltip for a time range bar that represents when the playback of a audio/video element is running"),
+                });
+                playingStartTime = NaN;
+            }
+
+            for (let item of timestamps) {
+                if (item.type === WI.MediaTimelineRecord.TimestampType.MediaElementDOMEvent) {
+                    if (WI.DOMNode.isPlayEvent(item.eventName)) {
+                        if (isNaN(playingStartTime))
+                            playingStartTime = item.timestamp;
+                        if (!isNaN(pausedStartTime))
+                            addPausedSegment(pausedStartTime, item.timestamp);
+                    } else if (WI.DOMNode.isPauseEvent(item.eventName)) {
+                        if (isNaN(pausedStartTime))
+                            pausedStartTime = item.timestamp;
+                        if (!isNaN(playingStartTime))
+                            addPlayingSegment(playingStartTime, item.timestamp);
+                    } else if (WI.DOMNode.isStopEvent(item.eventName)) {
+                        if (!isNaN(pausedStartTime))
+                            addPausedSegment(pausedStartTime, item.timestamp);
+                        if (!isNaN(playingStartTime))
+                            addPlayingSegment(playingStartTime, item.timestamp);
+                    } else if (item.eventName === "webkitfullscreenchange") {
+                        if (!fullScreenOriginator && item.originator)
+                            fullScreenOriginator = item.originator;
+
+                        if (isNaN(fullScreenStartTime)) {
+                            if (item.data && item.data.enabled)
+                                fullScreenStartTime = item.timestamp;
+                            else
+                                addFullScreenSegment(this.graphDataSource ? this.graphDataSource.startTime : record.startTime, item.timestamp);
+                        } else if (!item.data || !item.data.enabled)
+                            addFullScreenSegment(fullScreenStartTime, item.timestamp);
+                    }
+                } else if (item.type === WI.MediaTimelineRecord.TimestampType.MediaElementPowerEfficientPlaybackStateChange) {
+                    if (isNaN(powerEfficientPlaybackStartTime)) {
+                        if (item.isPowerEfficient)
+                            powerEfficientPlaybackStartTime = item.timestamp;
+                        else
+                            addPowerEfficientPlaybackSegment(this.graphDataSource ? this.graphDataSource.startTime : record.startTime, item.timestamp);
+                    } else if (!item.isPowerEfficient)
+                        addPowerEfficientPlaybackSegment(powerEfficientPlaybackStartTime, item.timestamp);
+                }
+            }
+
+            if (!isNaN(fullScreenStartTime))
+                addFullScreenSegment(fullScreenStartTime, NaN);
+            if (!isNaN(powerEfficientPlaybackStartTime))
+                addPowerEfficientPlaybackSegment(powerEfficientPlaybackStartTime, NaN);
+            if (!isNaN(pausedStartTime))
+                addPausedSegment(pausedStartTime, NaN);
+            if (!isNaN(playingStartTime))
+                addPlayingSegment(playingStartTime, NaN);
+
+            children.pushAll(fullScreenSegments);
+            children.pushAll(powerEfficientPlaybackSegments);
+            children.pushAll(activeSegments);
+            break;
+        }
+        }
+
+        timestamps.forEach((item, i) => {
+            let image = {
+                startTime: item.timestamp,
+                classNames: [],
+            };
+
+            switch (item.type) {
+            case WI.MediaTimelineRecord.TimestampType.CSSAnimationReady:
+            case WI.MediaTimelineRecord.TimestampType.CSSAnimationDelay:
+            case WI.MediaTimelineRecord.TimestampType.CSSAnimationDone:
+            case WI.MediaTimelineRecord.TimestampType.MediaElementPowerEfficientPlaybackStateChange:
+                // These timestamps are handled by the range segments above.
+                return;
+
+            case WI.MediaTimelineRecord.TimestampType.CSSAnimationActive:
+                // Don't create a marker segment for the first active timestamp, as that will be
+                // handled by an active range segment above.
+                if (!i || timestamps[i - 1].type !== WI.MediaTimelineRecord.TimestampType.CSSAnimationActive)
+                    return;
+
+                image.image = "Images/EventIteration.svg";
+                image.title = WI.UIString("Iteration", "Tooltip for a timestamp marker that represents when a CSS animation/transition iterates");
+                break;
+
+            case WI.MediaTimelineRecord.TimestampType.CSSAnimationCancel:
+                image.image = "Images/EventCancel.svg";
+                image.title = WI.UIString("Canceled", "Tooltip for a timestamp marker that represents when a CSS animation/transition is canceled");
+                break;
+
+            case WI.MediaTimelineRecord.TimestampType.MediaElementDOMEvent:
+                // Don't create a marker segment full-screen timestamps, as that will be handled by a
+                // range segment above.
+                if (item.eventName === "webkitfullscreenchange")
+                    return;
+
+                image.title = WI.UIString("DOM Event \u201C%s\u201D").format(item.eventName);
+                if (WI.DOMNode.isPlayEvent(item.eventName))
+                    image.image = "Images/EventPlay.svg";
+                else if (WI.DOMNode.isPauseEvent(item.eventName))
+                    image.image = "Images/EventPause.svg";
+                else if (WI.DOMNode.isStopEvent(item.eventName))
+                    image.image = "Images/EventStop.svg";
+                else
+                    image.image = "Images/EventProcessing.svg";
+                break;
+            }
+
+            children.push(image);
+        });
+
+        return children;
     }
 
     // Protected
 
     filterableDataForColumn(columnIdentifier)
     {
-        if (columnIdentifier === "element") {
+        switch (columnIdentifier) {
+        case "name":
+            return [this.record.displayName, this.record.subtitle];
+
+        case "element":
+        case "source": // Timeline Overview
             if (this.record.domNode)
                 return this.record.domNode.displayName;
         }
 
-        if (columnIdentifier === "originator") {
-            if (this.record.eventType === WI.MediaTimelineRecord.EventType.DOMEvent && this.record.domEvent.originator)
-                return this.record.domEvent.originator.displayName;
+        return super.filterableDataForColumn(columnIdentifier);
+    }
+
+    // Private
+
+    _createNameCellDocumentFragment()
+    {
+        let fragment = document.createDocumentFragment();
+        fragment.append(this.record.displayName);
+
+        if (this.record.subtitle) {
+            let subtitleElement = fragment.appendChild(document.createElement("span"));
+            subtitleElement.className = "subtitle";
+            subtitleElement.textContent = this.record.subtitle;
         }
 
-        return super.filterableDataForColumn(columnIdentifier);
+        return fragment;
     }
 };
index 8dddf13..865e556 100644 (file)
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-.timeline-overview-graph.media > .timeline-record-bar {
-    margin-top: 8px;
-    height: 20px;
+.timeline-overview-graph.media {
+    padding-top: 2px;
 }
 
-.timeline-overview-graph.media > .timeline-record-bar > .segment {
-    border-radius: 2px;
+.timeline-overview-graph.media > .graph-row {
+    height: 5px;
+}
+
+.timeline-overview-graph.media > .graph-row > .timeline-record-bar {
+    height: 4px;
+    margin-top: 1px;
+}
+
+.timeline-overview-graph.media > .graph-row > .timeline-record-bar:not(.unfinished) > .segment:not(.inactive) {
+    box-shadow: var(--timeline-odd-background-color) 1px 0 0;
+}
+
+.timeline-overview-graph.media:nth-child(even) > .graph-row > .timeline-record-bar:not(.unfinished) > .segment:not(.inactive) {
+    box-shadow: var(--timeline-even-background-color) 1px 0 0;
 }
index 4478aa9..02846eb 100644 (file)
@@ -32,35 +32,46 @@ WI.MediaTimelineOverviewGraph = class MediaTimelineOverviewGraph extends WI.Time
 
         super(timelineOverview);
 
-        this._timeline = timeline;
-        this._recordBars = [];
-
         this.element.classList.add("media");
 
         this.reset();
+
+        for (let record of timeline.records)
+            this._processRecord(record);
+
+        timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._handleRecordAdded, this);
+        timeline.addEventListener(WI.Timeline.Event.TimesUpdated, this._handleTimesUpdated, this);
+    }
+
+    // Static
+
+    static get maximumRowCount() {
+        return 6;
     }
 
     // Public
 
     reset()
     {
-        this.element.removeChildren();
-
         super.reset();
-    }
 
-    shown()
-    {
-        super.shown();
+        this._recordsWithoutStartTime = new Set;
 
-        this._timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._handleRecordAdded, this);
-    }
+        this.element.removeChildren();
 
-    hidden()
-    {
-        this._timeline.removeEventListener(null, null, this);
+        this._nextDumpRow = 0;
+        this._timelineRecordGridRows = [];
+
+        for (let i = 0; i < MediaTimelineOverviewGraph.maximumRowCount; ++i) {
+            let rowElement = this.element.appendChild(document.createElement("div"));
+            rowElement.className = "graph-row";
 
-        super.hidden();
+            this._timelineRecordGridRows.push({
+                records: [],
+                recordBars: [],
+                element: rowElement,
+            });
+        }
     }
 
     // Protected
@@ -70,27 +81,33 @@ WI.MediaTimelineOverviewGraph = class MediaTimelineOverviewGraph extends WI.Time
         if (!this.visible)
             return;
 
+        let secondsPerPixel = this.timelineOverview.secondsPerPixel;
         let recordBarIndex = 0;
 
-        let createBar = (records, renderMode) => {
-            let timelineRecordBar = this._recordBars[recordBarIndex];
-            if (timelineRecordBar) {
+        let createBar = (element, recordBars, records, renderMode) => {
+            let timelineRecordBar = recordBars[recordBarIndex];
+            if (!timelineRecordBar)
+                timelineRecordBar = recordBars[recordBarIndex] = new WI.TimelineRecordBar(this, records, renderMode);
+            else {
                 timelineRecordBar.renderMode = renderMode;
                 timelineRecordBar.records = records;
-            } else
-                timelineRecordBar = this._recordBars[recordBarIndex] = new WI.TimelineRecordBar(this, records, renderMode);
+            }
             timelineRecordBar.refresh(this);
             if (!timelineRecordBar.element.parentNode)
-                this.element.appendChild(timelineRecordBar.element);
+                element.appendChild(timelineRecordBar.element);
             ++recordBarIndex;
         };
 
-        WI.TimelineRecordBar.createCombinedBars(this._timeline.records, this.timelineOverview.secondsPerPixel, this, createBar);
+        for (let {records, recordBars, element} of this._timelineRecordGridRows) {
+            recordBarIndex = 0;
+
+            WI.TimelineRecordBar.createCombinedBars(records, secondsPerPixel, this, createBar.bind(this, element, recordBars));
 
-        // Remove the remaining unused TimelineRecordBars.
-        for (let i = recordBarIndex; i < this._recordBars.length; ++i) {
-            this._recordBars[i].records = null;
-            this._recordBars[i].element.remove();
+            // Remove the remaining unused `WI.TimelineRecordBar`.
+            for (; recordBarIndex < recordBars.length; ++recordBarIndex) {
+                recordBars[recordBarIndex].records = null;
+                recordBars[recordBarIndex].element.remove();
+            }
         }
     }
 
@@ -98,10 +115,12 @@ WI.MediaTimelineOverviewGraph = class MediaTimelineOverviewGraph extends WI.Time
     {
         super.updateSelectedRecord();
 
-        for (let recordBar of this._recordBars) {
-            if (recordBar.records.includes(this.selectedRecord)) {
-                this.selectedRecordBar = recordBar;
-                return;
+        for (let {recordBars} of this._timelineRecordGridRows) {
+            for (let recordBar of recordBars) {
+                if (recordBar.records.includes(this.selectedRecord)) {
+                    this.selectedRecordBar = recordBar;
+                    return;
+                }
             }
         }
 
@@ -110,8 +129,66 @@ WI.MediaTimelineOverviewGraph = class MediaTimelineOverviewGraph extends WI.Time
 
     // Private
 
+    _processRecord(record)
+    {
+        console.assert(record instanceof WI.MediaTimelineRecord);
+
+        if (isNaN(record.startTime)) {
+            this._recordsWithoutStartTime.add(record);
+            record.singleFireEventListener(WI.TimelineRecord.Event.Updated, (event) => {
+                this._processRecord(record);
+
+                this.needsLayout();
+            });
+            return;
+        }
+
+        this._recordsWithoutStartTime.delete(record);
+
+        function compareByStartTime(a, b) {
+            return a.startTime - b.startTime;
+        }
+
+        let minimumBarPaddingTime = WI.TimelineOverview.MinimumDurationPerPixel * (WI.TimelineRecordBar.MinimumWidthPixels + WI.TimelineRecordBar.MinimumMarginPixels);
+
+        // Try to find a row that has room and does not overlap a previous record.
+        for (let i = 0; i < this._timelineRecordGridRows.length; ++i) {
+            let records = this._timelineRecordGridRows[i].records;
+            let lastRecord = records.lastValue;
+            if (!lastRecord || lastRecord.endTime + minimumBarPaddingTime <= record.startTime) {
+                insertObjectIntoSortedArray(record, records, compareByStartTime);
+                this._nextDumpRow = i + 1;
+                return;
+            }
+        }
+
+        // Try to find a row that does not overlap a previous record's active time, but it can overlap the inactive time.
+        for (let i = 0; i < this._timelineRecordGridRows.length; ++i) {
+            let records = this._timelineRecordGridRows[i].records;
+            let lastRecord = records.lastValue;
+            console.assert(lastRecord);
+            if (lastRecord.usesActiveStartTime && lastRecord.activeStartTime + minimumBarPaddingTime <= record.startTime) {
+                insertObjectIntoSortedArray(record, records, compareByStartTime);
+                this._nextDumpRow = i + 1;
+                break;
+            }
+        }
+
+        // We didn't find a empty spot, so dump into the designated dump row.
+        if (this._nextDumpRow >= MediaTimelineOverviewGraph.maximumRowCount)
+            this._nextDumpRow = 0;
+        insertObjectIntoSortedArray(record, this._timelineRecordGridRows[this._nextDumpRow++].records, compareByStartTime);
+    }
+
     _handleRecordAdded(event)
     {
+        this._processRecord(event.data.record);
+
+        this.needsLayout();
+    }
+
+    _handleTimesUpdated(event)
+    {
         this.needsLayout();
     }
 };
index e7f9297..000c2d9 100644 (file)
@@ -46,30 +46,19 @@ WI.MediaTimelineView = class MediaTimelineView extends WI.TimelineView
                 title: WI.UIString("Element"),
                 width: "150px",
                 sortable: true,
-            },
-            time: {
-                title: WI.UIString("Time"),
-                width: "80px",
-                sortable: true,
                 locked: true,
             },
-            originator: {
-                title: WI.UIString("Originator"),
-                width: "150px",
-                sortable: true,
-                hidden: true,
-            },
             graph: {
                 headerView: this._timelineRuler,
+                sortable: true,
                 locked: true,
             },
         };
 
         this._dataGrid = new WI.TimelineDataGrid(columns);
         this._dataGrid.sortDelegate = this;
-        this._dataGrid.sortColumnIdentifier = "time";
+        this._dataGrid.sortColumnIdentifier = "graph";
         this._dataGrid.sortOrder = WI.DataGrid.SortOrder.Ascending;
-        this._dataGrid.setColumnVisible("originator", false);
         this._dataGrid.createSettings("media-timeline-view");
         this.setupDataGrid(this._dataGrid);
         this.addSubview(this._dataGrid);
@@ -98,18 +87,36 @@ WI.MediaTimelineView = class MediaTimelineView extends WI.TimelineView
         return [pathComponent];
     }
 
+    shown()
+    {
+        super.shown();
+
+        this._dataGrid.shown();
+    }
+
+    hidden()
+    {
+        this._dataGrid.hidden();
+
+        super.hidden();
+    }
+
     closed()
     {
-        this.representedObject.removeEventListener(null, null, this);
+        this.representedObject.removeEventListener(WI.Timeline.Event.RecordAdded, this._handleRecordAdded, this);
+
+        this._dataGrid.closed();
 
         super.closed();
     }
 
     reset()
     {
-        super.reset();
+        this._dataGrid.reset();
 
         this._pendingRecords = [];
+
+        super.reset();
     }
 
     // TimelineDataGrid delegate
@@ -123,7 +130,7 @@ WI.MediaTimelineView = class MediaTimelineView extends WI.TimelineView
                 return 1;
             if (!a && !b)
                 return 0;
-            return a.id - b.id;
+            return a.displayName.extendedLocaleCompare(b.displayName);
         }
 
         if (sortColumnIdentifier === "name") {
@@ -135,18 +142,9 @@ WI.MediaTimelineView = class MediaTimelineView extends WI.TimelineView
         if (sortColumnIdentifier === "element")
             return compareDOMNodes(node1.record.domNode, node2.record.domNode) * sortDirection;
 
-        if (sortColumnIdentifier === "time")
+        if (sortColumnIdentifier === "graph")
             return (node1.record.startTime - node2.record.startTime) * sortDirection;
 
-        if (sortColumnIdentifier === "originator") {
-            function getOriginator(record) {
-                if (record.eventType !== WI.MediaTimelineRecord.EventType.DOMEvent || !record.domEvent)
-                    return null;
-                return record.domEvent.originator;
-            }
-            return compareDOMNodes(getOriginator(node1.record), getOriginator(node2.record)) * sortDirection;
-        }
-
         return null;
     }
 
@@ -183,9 +181,6 @@ WI.MediaTimelineView = class MediaTimelineView extends WI.TimelineView
             return;
 
         for (let timelineRecord of this._pendingRecords) {
-            if (timelineRecord.domEvent && timelineRecord.domEvent.originator)
-                this._dataGrid.setColumnVisible("originator", true);
-
             this._dataGrid.addRowInSortOrder(new WI.MediaTimelineDataGridNode(timelineRecord, {
                 graphDataSource: this,
             }));
index f4b39c5..33076bf 100644 (file)
@@ -199,6 +199,11 @@ WI.TimelineDataGridNode = class TimelineDataGridNode extends WI.DataGridNode
             return fragment;
         }
 
+        if (value instanceof WI.DOMNode) {
+            cell.classList.add(WI.DOMTreeElementPathComponent.iconClassNameForNode(value));
+            return WI.linkifyNodeReference(value);
+        }
+
         return super.createCellContent(columnIdentifier, cell);
     }
 
index 7e41245..5d976b3 100644 (file)
     content: url(../Images/TimelineRecordTimer.svg);
 }
 
-.animation-record .icon {
-    content: url(../Images/TimelineRecordAnimation.svg);
+.animation-frame-record .icon {
+    content: url(../Images/TimelineRecordAnimationFrame.svg);
 }
 
-.heap-snapshot-record .icon {
-    content: url(../Images/HeapSnapshot.svg);
+.css-animation-record .icon {
+    content: url(../Images/TimelineRecordCSSAnimation.svg);
 }
 
-.dom-event-record .icon {
-    content: url(../Images/DOMEvent.svg);
+.css-transition-record .icon {
+    content: url(../Images/TimelineRecordCSSTransition.svg);
 }
 
-.dom-event-record.fullscreen .icon {
-    content: url(../Images/DOMEventFullscreen.svg);
+.media-element-record .icon {
+    content: url(../Images/TimelineRecordMediaElement.svg);
 }
 
-.power-efficient-playback-state-changed-record .icon {
-    content: url(../Images/PowerEfficientPlaybackStateChanged.svg);
+.heap-snapshot-record .icon {
+    content: url(../Images/HeapSnapshot.svg);
 }
 
 @media (prefers-color-scheme: dark) {
index cf2e0d8..85ca08a 100644 (file)
 
 .timeline-record-bar {
     position: absolute;
+    z-index: var(--timeline-record-z-index);
     height: 12px;
+}
+
+.timeline-record-bar > :matches(img, .segment) {
+    position: absolute;
     z-index: var(--timeline-record-z-index);
+}
 
-    --timeline-record-bar-has-inactive-segment-not-inactive-segment-border-start: 1px solid hsla(215, 67%, 53%, 0.7) !important;
+.timeline-record-bar > img {
+    width: 14px;
+    height: 14px;
+    margin-top: -1px;
+    -webkit-margin-start: -7px;
 }
 
 .timeline-record-bar > .segment {
-    position: absolute;
+    min-width: 4px;
     height: 100%;
     background-color: hsl(0, 0%, 88%);
-    border: 1px solid hsl(0, 0%, 78%);
-    border-radius: 3px;
-    min-width: 4px;
-    z-index: var(--timeline-record-z-index);
+    border-top: 1px solid var(--record-bar-segment-border-color);
+    border-bottom: 1px solid var(--record-bar-segment-border-color);
+
+    --record-bar-segment-border-color: hsl(0, 0%, 78%);
+    --record-bar-segment-border-start: 1px solid var(--record-bar-segment-border-color);
+    --record-bar-segment-border-end: 1px solid var(--record-bar-segment-border-color);
+    --record-bar-segment-border-start-radius: 3px;
+    --record-bar-segment-border-end-radius: 3px;
+}
+
+body[dir=ltr] .timeline-record-bar > .segment {
+    border-right: var(--record-bar-segment-border-end);
+    border-left: var(--record-bar-segment-border-start);
+}
+
+body[dir=ltr] .timeline-record-bar > .segment:first-of-type {
+    border-top-left-radius: var(--record-bar-segment-border-start-radius);
+    border-bottom-left-radius: var(--record-bar-segment-border-start-radius);
+}
+
+body[dir=ltr] .timeline-record-bar > .segment:last-of-type {
+    border-top-right-radius: var(--record-bar-segment-border-end-radius);
+    border-bottom-right-radius: var(--record-bar-segment-border-end-radius);
+}
+
+body[dir=rtl] .timeline-record-bar > .segment {
+    border-right: var(--record-bar-segment-border-start);
+    border-left: var(--record-bar-segment-border-end);
+}
+
+body[dir=rtl] .timeline-record-bar > .segment:first-of-type {
+    border-top-left-radius: var(--record-bar-segment-border-end-radius);
+    border-bottom-left-radius: var(--record-bar-segment-border-end-radius);
+}
+
+body[dir=rtl] .timeline-record-bar > .segment:last-of-type {
+    border-top-right-radius: var(--record-bar-segment-border-start-radius);
+    border-bottom-right-radius: var(--record-bar-segment-border-start-radius);
+}
+
+.timeline-record-bar > .segment:not(:last-of-type) {
+    --record-bar-segment-border-end: none;
 }
 
 .timeline-record-bar.selected > .segment {
     background-color: var(--selected-text-background-color) !important;
-    border-color: var(--glyph-color-active) !important;
+    --record-bar-segment-border-color: var(--glyph-color-active) !important;
 }
 
 .timeline-record-bar:not(.has-inactive-segment) > .segment {
     z-index: 0;
 }
 
-/*
-The selector `.timeline-record-bar.has-inactive-segment > .segment:not(.inactive)` is used to
-differentiate between the "active" and "inactive" parts of a network entry.  As an example, the
-"active" part would be the Response and the "inactive" part would be the Request.  This selector
-ensures that there is a different style between "active" and "inactive" only when the network entry
-has an "inactive" segment.
-*/
-
-body[dir=ltr] .timeline-record-bar > .segment.inactive,
-body[dir=ltr] .timeline-record-bar.unfinished > .segment,
-body[dir=rtl] .timeline-record-bar.has-inactive-segment > .segment:not(.inactive) {
-    border-top-right-radius: 0 !important;
-    border-bottom-right-radius: 0 !important;
-    border-right: none;
+.timeline-record-bar > .segment.inactive,
+.timeline-record-bar.unfinished > .segment:last-child {
+    --record-bar-segment-border-end: none;
+    --record-bar-segment-border-end-radius: 0;
 }
 
-body[dir=ltr] .timeline-record-bar.has-inactive-segment > .segment:not(.inactive),
-body[dir=rtl] .timeline-record-bar > .segment.inactive,
-body[dir=rtl] .timeline-record-bar.unfinished > .segment {
-    border-top-left-radius: 0 !important;
-    border-bottom-left-radius: 0 !important;
+.timeline-record-bar.has-inactive-segment > .segment:not(.inactive) {
+    --record-bar-segment-border-start-radius: 0;
 }
 
-:focus .selected .timeline-record-bar > .segment {
+:focus .selected .timeline-record-bar:not(.has-custom-children) > .segment {
     background-color: white !important;
     border: none !important;
 }
 
-:focus .selected .timeline-record-bar > .segment.inactive {
+:focus .selected .timeline-record-bar:not(.has-custom-children) > .segment.inactive {
     background-color: hsl(215, 63%, 85%) !important;
 }
 
-body[dir=ltr] :focus .selected .timeline-record-bar.has-inactive-segment > .segment:not(.inactive) {
-    border-left: var(--timeline-record-bar-has-inactive-segment-not-inactive-segment-border-start);
-}
-
-body[dir=rtl] :focus .selected .timeline-record-bar.has-inactive-segment > .segment:not(.inactive) {
-    border-right: var(--timeline-record-bar-has-inactive-segment-not-inactive-segment-border-start);
+:focus .selected .timeline-record-bar.has-inactive-segment > .segment:not(.inactive) {
+    --record-bar-segment-border-start: 1px solid hsla(215, 67%, 53%, 0.7);
 }
 
 .timeline-record-bar.timeline-record-type-network > .segment {
     background-color: hsl(207, 63%, 67%);
-    border-color: hsl(202, 55%, 51%);
+    --record-bar-segment-border-color: hsl(202, 55%, 51%);
 }
 
 .timeline-record-bar.timeline-record-type-network > .segment.inactive {
     background-color: hsl(208, 66%, 79%);
-    border-color: hsl(202, 57%, 68%);
+    --record-bar-segment-border-color: hsl(202, 57%, 68%);
 }
 
 .timeline-record-bar.timeline-record-type-layout > .segment {
     background-color: hsl(0, 65%, 75%);
-    border-color: hsl(0, 54%, 62%);
+    --record-bar-segment-border-color: hsl(0, 54%, 62%);
 }
 
 
 .timeline-record-bar.timeline-record-type-layout.paint > .segment,
 .timeline-record-bar.timeline-record-type-layout.composite > .segment {
     background-color: hsl(76, 49%, 60%);
-    border-color: hsl(79, 45%, 51%);
+    --record-bar-segment-border-color: hsl(79, 45%, 51%);
 }
 
 .timeline-record-bar.timeline-record-type-script > .segment {
     background-color: hsl(269, 65%, 74%);
-    border-color: hsl(273, 33%, 58%);
+    --record-bar-segment-border-color: hsl(273, 33%, 58%);
 }
 
 .timeline-record-bar.timeline-record-type-script.garbage-collected > .segment,
 .timeline-record-bar.timeline-record-type-heap-allocations > .segment {
     background-color: hsl(23, 69%, 73%);
-    border-color: hsl(11, 54%, 62%);
+    --record-bar-segment-border-color: hsl(11, 54%, 62%);
 }
 
 .timeline-record-bar.timeline-record-type-media > .segment {
     background-color: hsl(143, 24%, 66%);
-    border-color: hsl(153, 24%, 51%);
+    --record-bar-segment-border-color: hsl(153, 24%, 51%);
+}
+
+.timeline-record-bar.has-custom-children.timeline-record-type-media > .segment {
+    height: 8px;
+    margin-top: 2px;
+}
+
+.timeline-record-bar.has-custom-children.timeline-record-type-media > .segment.css-animation-ready {
+    background-color: var(--text-color-quaternary);
+}
+
+.timeline-record-bar.has-custom-children.timeline-record-type-media > .segment:matches(.css-animation-delay, .media-element-paused) {
+    background-color: hsl(143, 24%, 84%);
+}
+
+.timeline-record-bar.has-custom-children.timeline-record-type-media.media-element > .segment {
+    --record-bar-segment-border-start-radius: 0;
+    --record-bar-segment-border-end-radius: 0;
+}
+
+.timeline-record-bar.has-custom-children.timeline-record-type-media.media-element > .segment.media-element-full-screen {
+    height: 14px;
+    margin-top: -1px;
+    background-color: lightgrey;
+    border: none;
+}
+
+.timeline-record-bar.has-custom-children.timeline-record-type-media.media-element > .segment.media-element-power-efficient-playback {
+    height: 12px;
+    margin-top: 0;
+    background-color: var(--network-request-color);
+    border: none;
 }
index 02786b9..190aa03 100644 (file)
@@ -29,7 +29,7 @@ WI.TimelineRecordBar = class TimelineRecordBar extends WI.Object
     {
         super();
 
-        this._delegate = delegate;
+        this._delegate = delegate || null;
 
         this._element = document.createElement("div");
         this._element.classList.add("timeline-record-bar");
@@ -236,8 +236,10 @@ WI.TimelineRecordBar = class TimelineRecordBar extends WI.Object
                 this._element.classList.remove(oldRecordEventType);
                 this._element.classList.add(newRecord.eventType);
             }
-            if (newRecord.usesActiveStartTime !== oldRecordUsesActiveStartTime)
-                this._element.classList.toggle("has-inactive-segment", newRecord.usesActiveStartTime);
+            if (newRecord.usesActiveStartTime !== oldRecordUsesActiveStartTime) {
+                if (!this._delegate || !this._delegate.timelineRecordBarCustomChildren)
+                    this._element.classList.toggle("has-inactive-segment", newRecord.usesActiveStartTime);
+            }
         } else
             this._element.classList.remove(oldRecordType, oldRecordEventType, "has-inactive-segment");
     }
@@ -245,7 +247,7 @@ WI.TimelineRecordBar = class TimelineRecordBar extends WI.Object
     refresh(graphDataSource)
     {
         if (isNaN(graphDataSource.secondsPerPixel))
-            return;
+            return false;
 
         console.assert(graphDataSource.zeroTime);
         console.assert(graphDataSource.startTime);
@@ -281,6 +283,8 @@ WI.TimelineRecordBar = class TimelineRecordBar extends WI.Object
         if (barUnfinished)
             barEndTime = graphCurrentTime;
 
+        var barDuration = barEndTime - barStartTime;
+
         var graphDuration = graphEndTime - graphStartTime;
 
         let newBarPosition = (barStartTime - graphStartTime) / graphDuration;
@@ -290,6 +294,36 @@ WI.TimelineRecordBar = class TimelineRecordBar extends WI.Object
         var newBarWidth = ((barEndTime - graphStartTime) / graphDuration) - newBarPosition;
         this._updateElementPosition(this._element, newBarWidth, "width");
 
+        if (this._delegate && this._delegate.timelineRecordBarCustomChildren) {
+            this._element.removeChildren();
+
+            this._element.classList.add("has-custom-children");
+            this._element.classList.toggle("unfinished", barUnfinished);
+
+            let children = this._delegate.timelineRecordBarCustomChildren(this);
+            for (let child of children) {
+                let childElement;
+                if (child.image) {
+                    childElement = this._element.appendChild(document.createElement("img"));
+                    childElement.src = child.image;
+                } else
+                    childElement = this._element.appendChild(document.createElement("div"));
+
+                childElement.classList.add(...child.classNames);
+                childElement.title = child.title;
+                this._updateElementPosition(childElement, (child.startTime - barStartTime) / barDuration, property);
+
+                if (typeof child.endTime === "number") {
+                    let childEndTime = !isNaN(child.endTime) ? child.endTime : barEndTime;
+                    this._updateElementPosition(childElement, (childEndTime - child.startTime) / barDuration, "width");
+                }
+            }
+
+            return true;
+        }
+
+        this._element.classList.remove("has-custom-children");
+
         if (!this._activeBarElement && this._renderMode !== WI.TimelineRecordBar.RenderMode.InactiveOnly) {
             this._activeBarElement = document.createElement("div");
             this._activeBarElement.classList.add("segment");
@@ -325,8 +359,6 @@ WI.TimelineRecordBar = class TimelineRecordBar extends WI.Object
         else
             var barActiveStartTime = this._records.reduce(function(previousValue, currentValue) { return Math.max(previousValue, currentValue.activeStartTime); }, 0);
 
-        var barDuration = barEndTime - barStartTime;
-
         var inactiveUnfinished = isNaN(barActiveStartTime) || barActiveStartTime >= graphCurrentTime;
         this._element.classList.toggle("unfinished", inactiveUnfinished);
 
@@ -391,7 +423,7 @@ WI.TimelineRecordBar = class TimelineRecordBar extends WI.Object
         // Ensure that the container "click" listener added by `WI.TimelineOverview` isn't called.
         event.__timelineRecordClickEventHandled = true;
 
-        if (this._delegate.timelineRecordBarClicked)
+        if (this._delegate && this._delegate.timelineRecordBarClicked)
             this._delegate.timelineRecordBarClicked(this);
     }
 };
index 2eea55f..0585752 100644 (file)
@@ -110,7 +110,6 @@ WI.TimelineRecordTreeElement.APIRecordIconStyleClass = "api-record";
 WI.TimelineRecordTreeElement.EvaluatedRecordIconStyleClass = "evaluated-record";
 WI.TimelineRecordTreeElement.EventRecordIconStyleClass = "event-record";
 WI.TimelineRecordTreeElement.TimerRecordIconStyleClass = "timer-record";
-WI.TimelineRecordTreeElement.AnimationRecordIconStyleClass = "animation-record";
 WI.TimelineRecordTreeElement.ProbeRecordIconStyleClass = "probe-record";
 WI.TimelineRecordTreeElement.ConsoleProfileIconStyleClass = "console-profile-record";
 WI.TimelineRecordTreeElement.GarbageCollectionIconStyleClass = "garbage-collection-profile-record";
index 8a44113..c739625 100644 (file)
@@ -131,6 +131,9 @@ WI.TimelineTabContentView = class TimelineTabContentView extends WI.ContentBrows
         case WI.TimelineRecord.Type.HeapAllocations:
             return WI.UIString("JavaScript Allocations");
         case WI.TimelineRecord.Type.Media:
+            // COMPATIBILITY (iOS 13): Animation domain did not exist yet.
+            if (InspectorBackend.hasDomain("Animation"))
+                return WI.UIString("Media & Animations");
             return WI.UIString("Media");
         default:
             console.error("Unknown Timeline type:", timelineType);
@@ -237,7 +240,7 @@ WI.TimelineTabContentView = class TimelineTabContentView extends WI.ContentBrows
             case WI.ScriptTimelineRecord.EventType.AnimationFrameFired:
             case WI.ScriptTimelineRecord.EventType.AnimationFrameRequested:
             case WI.ScriptTimelineRecord.EventType.AnimationFrameCanceled:
-                return WI.TimelineRecordTreeElement.AnimationRecordIconStyleClass;
+                return "animation-frame-record";
             default:
                 console.error("Unknown ScriptTimelineRecord eventType: " + timelineRecord.eventType, timelineRecord);
             }
@@ -252,10 +255,12 @@ WI.TimelineTabContentView = class TimelineTabContentView extends WI.ContentBrows
 
         case WI.TimelineRecord.Type.Media:
             switch (timelineRecord.eventType) {
-            case WI.MediaTimelineRecord.EventType.DOMEvent:
-                return "dom-event-record";
-            case WI.MediaTimelineRecord.EventType.PowerEfficientPlaybackStateChanged:
-                return "power-efficient-playback-state-changed-record";
+            case WI.MediaTimelineRecord.EventType.CSSAnimation:
+                return "css-animation-record";
+            case WI.MediaTimelineRecord.EventType.CSSTransition:
+                return "css-transition-record";
+            case WI.MediaTimelineRecord.EventType.MediaElement:
+                return "media-element-record";
             default:
                 console.error("Unknown MediaTimelineRecord eventType: " + timelineRecord.eventType, timelineRecord);
             }
@@ -291,6 +296,10 @@ WI.TimelineTabContentView = class TimelineTabContentView extends WI.ContentBrows
                 return WI.UIString("Snapshot %d \u2014 %s").format(timelineRecord.heapSnapshot.identifier, timelineRecord.heapSnapshot.title);
             return WI.UIString("Snapshot %d").format(timelineRecord.heapSnapshot.identifier);
         case WI.TimelineRecord.Type.Media:
+            // Since the `displayName` can be specific to an `animation-name`/`transition-property`,
+            // use the generic `subtitle` text instead of we are rendering from the overview.
+            if (includeDetailsInMainTitle && timelineRecord.subtitle)
+                return timelineRecord.subtitle;
             return timelineRecord.displayName;
         case WI.TimelineRecord.Type.CPU:
         case WI.TimelineRecord.Type.Memory:
index 9e08c40..a54598f 100644 (file)
@@ -1,3 +1,15 @@
+2019-11-01  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Timelines: add a timeline that shows information about any recorded CSS animation/transition
+        https://bugs.webkit.org/show_bug.cgi?id=203651
+        <rdar://problem/56128726>
+
+        Reviewed by Brian Burg.
+
+        * TestWebKitAPI/Tests/WTF/Markable.cpp:
+        (TestWebKitAPI::TEST):
+        Add tests for extra utility operators.
+
 2019-11-01  Brady Eidson  <beidson@apple.com>
 
         Promote "_getWebArchive" to API.
index 2b23a8e..b935575 100644 (file)
@@ -259,4 +259,60 @@ TEST(WTF_Markable, MoveOptional)
     }
 }
 
+TEST(WTF_Markable, Equality)
+{
+    Markable<int, IntegralMarkableTraits<int, 42>> unengaged1;
+    Markable<int, IntegralMarkableTraits<int, 42>> unengaged2;
+
+    Markable<int, IntegralMarkableTraits<int, 42>> engaged1 { 1 };
+    Markable<int, IntegralMarkableTraits<int, 42>> engaged2 { 2 };
+    Markable<int, IntegralMarkableTraits<int, 42>> engagedx2 { 2 };
+
+    EXPECT_TRUE(unengaged1 == unengaged2);
+    EXPECT_FALSE(engaged1 == engaged2);
+    EXPECT_FALSE(engaged1 == unengaged1);
+    EXPECT_TRUE(engaged2 == engagedx2);
+
+    EXPECT_FALSE(unengaged1 == 42);
+    EXPECT_FALSE(engaged1 == 42);
+    EXPECT_FALSE(42 == unengaged1);
+    EXPECT_FALSE(42 == engaged1);
+
+    EXPECT_TRUE(engaged1 == 1);
+    EXPECT_FALSE(engaged1 == 2);
+    EXPECT_TRUE(1 == engaged1);
+    EXPECT_FALSE(2 == engaged1);
+
+    EXPECT_FALSE(unengaged1 == 1);
+    EXPECT_FALSE(1 == unengaged1);
+}
+
+TEST(WTF_Markable, Inequality)
+{
+    Markable<int, IntegralMarkableTraits<int, 42>> unengaged1;
+    Markable<int, IntegralMarkableTraits<int, 42>> unengaged2;
+
+    Markable<int, IntegralMarkableTraits<int, 42>> engaged1 { 1 };
+    Markable<int, IntegralMarkableTraits<int, 42>> engaged2 { 2 };
+    Markable<int, IntegralMarkableTraits<int, 42>> engagedx2 { 2 };
+
+    EXPECT_FALSE(unengaged1 != unengaged2);
+    EXPECT_TRUE(engaged1 != engaged2);
+    EXPECT_TRUE(engaged1 != unengaged1);
+    EXPECT_FALSE(engaged2 != engagedx2);
+
+    EXPECT_TRUE(unengaged1 != 42);
+    EXPECT_TRUE(engaged1 != 42);
+    EXPECT_TRUE(42 != unengaged1);
+    EXPECT_TRUE(42 != engaged1);
+
+    EXPECT_FALSE(engaged1 != 1);
+    EXPECT_TRUE(engaged1 != 2);
+    EXPECT_FALSE(1 != engaged1);
+    EXPECT_TRUE(2 != engaged1);
+
+    EXPECT_TRUE(unengaged1 != 1);
+    EXPECT_TRUE(1 != unengaged1);
+}
+
 } // namespace TestWebKitAPI