Skip to content

Commit 7b95374

Browse files
committed
[Site Isolation] Web Inspector: Introduce a Timeline agent for frame targets
rdar://191252635 https://bugs.webkit.org/show_bug.cgi?id=329362 Reviewed by NOBODY (OOPS!). Introduce FrameTimelineAgent to report each frame's Timeline records on its own frame target under Site Isolation. Before this, Timeline records from cross-origin iframes were not reported at all. Under Site Isolation, make a frame's Timeline instrumentation for timers, events, script, animation frames and console never fall back to the page's agent, so each of these records reaches the frame's own agent or nothing. The rule is set before any script runs in the frame, so a "will" hook and its "did" hook always reach the same agent. The page's agent stays registered for page-level records (rendering frames, composite and screenshots), auto-capture, and layout, paint and style records until those move to frame targets. A frame's agent starts only the Timeline instrument for now, because the other instruments would start the page's agents. Under Site Isolation, this removes the ScriptProfiler and Heap start events from inspector/timeline/setInstruments-programmatic-capture.html. In the frontend, the Script timeline adds a target on its first record, not only on its first sample. Under Site Isolation, the Script view shows a frame target by default, because the page target reports only page-level data there, such as garbage collections and microtasks. Tests: http/tests/site-isolation/inspector/timeline/frame-added-and-removed-during-recording.html http/tests/site-isolation/inspector/timeline/frame-target-records.html http/tests/site-isolation/inspector/timeline/provisional-frame-target-removed-during-recording.html http/tests/site-isolation/inspector/timeline/same-process-frame-added-during-recording.html * Source/JavaScriptCore/inspector/protocol/Timeline.json: * Source/WebCore/Sources.txt: * Source/WebCore/WebCore.xcodeproj/project.pbxproj: * Source/WebCore/inspector/agents/frame/FrameTimelineAgent.cpp: Added. (WebCore::FrameTimelineAgent::FrameTimelineAgent): (WebCore::FrameTimelineAgent::setAutoCaptureEnabled): (WebCore::FrameTimelineAgent::shouldToggleInstrument const): (WebCore::FrameTimelineAgent::nodeIdForNode): * Source/WebCore/inspector/agents/frame/FrameTimelineAgent.h: Added. Add the Timeline agent for frame targets. It toggles only the Timeline instrument, because the other instruments belong to domains that frame targets do not have yet, so their getters would reach the page's agents. It sends no node in Largest Contentful Paint records yet, because the frontend resolves node IDs only through the page target. * Source/WebCore/inspector/agents/InspectorTimelineAgent.cpp: (WebCore::InspectorTimelineAgent::didEnqueueLargestContentfulPaint): (WebCore::InspectorTimelineAgent::nodeIdForNode): (WebCore::InspectorTimelineAgent::toggleScriptProfilerInstrument): (WebCore::InspectorTimelineAgent::toggleHeapInstrument): (WebCore::InspectorTimelineAgent::toggleCPUInstrument): (WebCore::InspectorTimelineAgent::toggleMemoryInstrument): (WebCore::InspectorTimelineAgent::toggleAnimationInstrument): * Source/WebCore/inspector/agents/InspectorTimelineAgent.h: (WebCore::InspectorTimelineAgent::shouldToggleInstrument const): Add two virtual functions for FrameTimelineAgent to override: shouldToggleInstrument(), checked before toggling each instrument other than Timeline, and nodeIdForNode(), whose default keeps the existing Largest Contentful Paint behavior. * Source/WebCore/inspector/InstrumentingAgents.cpp: (WebCore::InstrumentingAgents::siteIsolationFirstEnabled): * Source/WebCore/inspector/InstrumentingAgents.h: Under Site Isolation, a frame's enabled and tracking Timeline slots no longer fall back to the page's. A Timeline agent fills its tracking slot only while it is recording, and the page's agent records in almost every recording for its page-level records, so with the fallback a frame's records would go to the page's agent whenever the frame's own agent is not recording, such as before the frame target starts or after it stops. * Source/WebCore/inspector/FrameInspectorController.cpp: (WebCore::FrameInspectorController::siteIsolationFirstEnabled): Turn on the rule above, before any script runs in the frame. (WebCore::FrameInspectorController::createLazyAgents): Create the FrameTimelineAgent after the frame's debugger, which it observes while recording. * Source/WebInspectorUI/UserInterface/Models/ScriptTimeline.js: (WI.ScriptTimeline.prototype.addRecord): (WI.ScriptTimeline.prototype.updateCallingContextTrees): (WI.ScriptTimeline.prototype._createCallingContextTrees): (WI.ScriptTimeline.prototype._addTarget): * Source/WebInspectorUI/UserInterface/Views/ScriptClusterTimelineView.js: (WI.ScriptClusterTimelineView): (WI.ScriptClusterTimelineView.prototype._sortedTargetPathComponents): (WI.ScriptClusterTimelineView.prototype._sortTargetPathComponents): (WI.ScriptClusterTimelineView.prototype._defaultTarget): (WI.ScriptClusterTimelineView.prototype._handleTargetAdded): * LayoutTests/http/tests/site-isolation/inspector/timeline/frame-added-and-removed-during-recording-expected.txt: Added. * LayoutTests/http/tests/site-isolation/inspector/timeline/frame-added-and-removed-during-recording.html: Added. * LayoutTests/http/tests/site-isolation/inspector/timeline/frame-target-records-expected.txt: Added. * LayoutTests/http/tests/site-isolation/inspector/timeline/frame-target-records.html: Added. * LayoutTests/http/tests/site-isolation/inspector/timeline/frame-target-timestamps.html: * LayoutTests/http/tests/site-isolation/inspector/timeline/provisional-frame-target-removed-during-recording-expected.txt: Added. * LayoutTests/http/tests/site-isolation/inspector/timeline/provisional-frame-target-removed-during-recording.html: Added. * LayoutTests/http/tests/site-isolation/inspector/timeline/resources/frame-timeline-test-utilities.js: Added. * LayoutTests/http/tests/site-isolation/inspector/timeline/resources/timeline-events.js: Added. * LayoutTests/http/tests/site-isolation/inspector/timeline/resources/timeline-frame.html: Added. * LayoutTests/http/tests/site-isolation/inspector/timeline/same-process-frame-added-during-recording-expected.txt: Added. * LayoutTests/http/tests/site-isolation/inspector/timeline/same-process-frame-added-during-recording.html: Added. * LayoutTests/inspector/timeline/timeline-recording-multiple-targets-expected.txt: * LayoutTests/inspector/timeline/timeline-recording-multiple-targets.html: * LayoutTests/platform/mac-site-isolation/TestExpectations: * LayoutTests/platform/mac-site-isolation/inspector/timeline/setInstruments-programmatic-capture-expected.txt: Added.
1 parent 66576ac commit 7b95374

28 files changed

Lines changed: 843 additions & 29 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Test that under Site Isolation, a cross-origin iframe added during a Timeline recording is recorded, and that removing it neither stops the recording nor keeps it from stopping.
2+
3+
4+
== Running test suite: SiteIsolation.Timeline.FrameAddedAndRemovedDuringRecording
5+
-- Running test case: SiteIsolation.Timeline.FrameAddedAndRemovedDuringRecording.Added
6+
PASS: Should find the frame target of the iframe added during the recording.
7+
PASS: The frame target of the added iframe started tracking.
8+
PASS: Recorded a timer from the frame target of the added iframe.
9+
10+
-- Running test case: SiteIsolation.Timeline.FrameAddedAndRemovedDuringRecording.Removed
11+
PASS: The frame target of the removed iframe was removed.
12+
PASS: Capturing should still be active.
13+
PASS: Recorded a timer from the main frame target after the iframe was removed.
14+
PASS: Capturing stopped.
15+
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<script src="../../../inspector/resources/inspector-test.js"></script>
4+
<script src="../debugger/resources/site-isolation-debugger-test-utilities.js"></script>
5+
<script src="resources/frame-timeline-test-utilities.js"></script>
6+
<script src="resources/timeline-events.js"></script>
7+
<script>
8+
let iframe;
9+
10+
function addCrossOriginIFrame()
11+
{
12+
iframe = document.createElement("iframe");
13+
iframe.src = "http://localhost:8000/site-isolation/inspector/timeline/resources/timeline-frame.html";
14+
document.body.appendChild(iframe);
15+
}
16+
17+
function removeIFrame()
18+
{
19+
iframe.remove();
20+
}
21+
22+
function test() {
23+
let suite = InspectorTest.createAsyncSuite("SiteIsolation.Timeline.FrameAddedAndRemovedDuringRecording");
24+
25+
let mainFrameTarget;
26+
let iframeTarget;
27+
28+
suite.addTestCase({
29+
name: "SiteIsolation.Timeline.FrameAddedAndRemovedDuringRecording.Added",
30+
description: "A cross-origin iframe added during a recording should be recorded by its frame target.",
31+
async test() {
32+
mainFrameTarget = InspectorTest.mainFrameTarget;
33+
await FrameTimelineTest.startCapturing([mainFrameTarget]);
34+
35+
await FrameTimelineTest.evaluateInTarget(mainFrameTarget, "addCrossOriginIFrame()");
36+
iframeTarget = await waitForFrameTargetForURLContaining("timeline-frame.html");
37+
InspectorTest.expectNotNull(iframeTarget, "Should find the frame target of the iframe added during the recording.");
38+
39+
await FrameTimelineTest.awaitTrackingEvent("capturingStarted", iframeTarget);
40+
InspectorTest.pass("The frame target of the added iframe started tracking.");
41+
42+
// The frame target can be found before the iframe's scripts have loaded.
43+
await waitForExpressionInTarget(iframeTarget, "typeof fireTimer === 'function'");
44+
45+
let recordPromise = FrameTimelineTest.awaitScriptRecord(iframeTarget, WI.ScriptTimelineRecord.EventType.TimerFired);
46+
await FrameTimelineTest.evaluateInTarget(iframeTarget, "fireTimer()");
47+
await recordPromise;
48+
InspectorTest.pass("Recorded a timer from the frame target of the added iframe.");
49+
}
50+
});
51+
52+
suite.addTestCase({
53+
name: "SiteIsolation.Timeline.FrameAddedAndRemovedDuringRecording.Removed",
54+
description: "Removing the cross-origin iframe should not stop the recording, and the recording should still stop.",
55+
async test() {
56+
let targetRemovedPromise = new Promise((resolve) => {
57+
let listener = WI.targetManager.addEventListener(WI.TargetManager.Event.TargetRemoved, (event) => {
58+
if (event.data.target !== iframeTarget)
59+
return;
60+
61+
WI.targetManager.removeEventListener(WI.TargetManager.Event.TargetRemoved, listener);
62+
resolve();
63+
});
64+
});
65+
await FrameTimelineTest.evaluateInTarget(mainFrameTarget, "removeIFrame()");
66+
await targetRemovedPromise;
67+
InspectorTest.pass("The frame target of the removed iframe was removed.");
68+
InspectorTest.expectEqual(WI.timelineManager.capturingState, WI.TimelineManager.CapturingState.Active, "Capturing should still be active.");
69+
70+
let recordPromise = FrameTimelineTest.awaitScriptRecord(mainFrameTarget, WI.ScriptTimelineRecord.EventType.TimerFired);
71+
await FrameTimelineTest.evaluateInTarget(mainFrameTarget, "fireTimer()");
72+
await recordPromise;
73+
InspectorTest.pass("Recorded a timer from the main frame target after the iframe was removed.");
74+
75+
await FrameTimelineTest.stopCapturing();
76+
InspectorTest.pass("Capturing stopped.");
77+
}
78+
});
79+
80+
suite.runTestCasesAndFinish();
81+
}
82+
</script>
83+
84+
<body onload="runTest()">
85+
<p>Test that under Site Isolation, a cross-origin iframe added during a Timeline recording is recorded, and that removing it neither stops the recording nor keeps it from stopping.</p>
86+
</body>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
Test that under Site Isolation, Timeline records are reported by the frame target of the frame that did the work, including a cross-origin iframe.
2+
3+
4+
5+
== Running test suite: SiteIsolation.Timeline.FrameTargetRecords
6+
-- Running test case: SiteIsolation.Timeline.FrameTargetRecords.Domain
7+
PASS: Should find the cross-origin iframe target.
8+
PASS: The cross-origin iframe target should not be the main frame target.
9+
PASS: The main frame target should have the Timeline domain.
10+
PASS: The main frame target should not have Timeline.setAutoCaptureEnabled.
11+
PASS: The cross-origin iframe target should have the Timeline domain.
12+
PASS: The cross-origin iframe target should not have Timeline.setAutoCaptureEnabled.
13+
PASS: The page target should still have Timeline.setAutoCaptureEnabled.
14+
15+
-- Running test case: SiteIsolation.Timeline.FrameTargetRecords.Timers
16+
PASS: Both frame targets started tracking.
17+
PASS: Recorded a timer from the main frame target.
18+
PASS: Recorded a timer from the cross-origin iframe target.
19+
PASS: Capturing stopped.
20+
PASS: The main frame target should report that Timeline stopped.
21+
PASS: The cross-origin iframe target should report that Timeline stopped.
22+
PASS: Record from the main frame target should have a non-zero start time.
23+
PASS: Record from the main frame target should be within the recording.
24+
PASS: Record from the cross-origin iframe target should have a non-zero start time.
25+
PASS: Record from the cross-origin iframe target should be within the recording.
26+
PASS: No script records other than garbage collections should come from the page target.
27+
PASS: The Script timeline should include the main frame target.
28+
PASS: The Script timeline should include the cross-origin iframe target.
29+
30+
-- Running test case: SiteIsolation.Timeline.FrameTargetRecords.EventDispatch
31+
PASS: Recorded the dispatch of the test event from the cross-origin iframe target.
32+
PASS: Capturing stopped.
33+
PASS: Record from the cross-origin iframe target should have a non-zero start time.
34+
PASS: Record from the cross-origin iframe target should be within the recording.
35+
PASS: No script records other than garbage collections should come from the page target.
36+
37+
-- Running test case: SiteIsolation.Timeline.FrameTargetRecords.NoFallbackToPageTarget
38+
PASS: The main frame target should not be tracking.
39+
PASS: A timer fired in the main frame.
40+
PASS: Capturing stopped.
41+
PASS: The page target should not record the main frame's timer.
42+
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<script src="../../../inspector/resources/inspector-test.js"></script>
4+
<script src="../debugger/resources/site-isolation-debugger-test-utilities.js"></script>
5+
<script src="resources/frame-timeline-test-utilities.js"></script>
6+
<script src="resources/timeline-events.js"></script>
7+
<script>
8+
function test() {
9+
let suite = InspectorTest.createAsyncSuite("SiteIsolation.Timeline.FrameTargetRecords");
10+
11+
let mainFrameTarget;
12+
let iframeTarget;
13+
14+
function targetDescription(target) {
15+
if (target === mainFrameTarget)
16+
return "main frame target";
17+
if (target === iframeTarget)
18+
return "cross-origin iframe target";
19+
if (target === WI.mainTarget)
20+
return "page target";
21+
return "unknown target";
22+
}
23+
24+
function expectRecordInRecording(record, startTime, endTime) {
25+
let description = targetDescription(record.target);
26+
InspectorTest.expectThat(record.startTime > 0, `Record from the ${description} should have a non-zero start time.`);
27+
InspectorTest.expectThat(startTime <= record.startTime && record.endTime <= endTime, `Record from the ${description} should be within the recording.`);
28+
}
29+
30+
function expectNoScriptRecordsFromPageTarget(recording) {
31+
let scriptTimeline = recording.timelines.get(WI.TimelineRecord.Type.Script);
32+
// Garbage collections are reported by the page target's Heap domain.
33+
let pageTargetRecords = scriptTimeline.records.filter((record) => record.target === WI.mainTarget && record.eventType !== WI.ScriptTimelineRecord.EventType.GarbageCollected);
34+
InspectorTest.expectEqual(pageTargetRecords.length, 0, "No script records other than garbage collections should come from the page target.");
35+
}
36+
37+
suite.addTestCase({
38+
name: "SiteIsolation.Timeline.FrameTargetRecords.Domain",
39+
description: "Frame targets should have the Timeline domain, but not auto-capture.",
40+
async test() {
41+
mainFrameTarget = InspectorTest.mainFrameTarget;
42+
iframeTarget = await waitForFrameTargetForURLContaining("timeline-frame.html");
43+
InspectorTest.expectNotNull(iframeTarget, "Should find the cross-origin iframe target.");
44+
InspectorTest.expectNotEqual(iframeTarget, mainFrameTarget, "The cross-origin iframe target should not be the main frame target.");
45+
46+
for (let target of [mainFrameTarget, iframeTarget]) {
47+
let description = targetDescription(target);
48+
InspectorTest.expectThat(target.hasDomain("Timeline"), `The ${description} should have the Timeline domain.`);
49+
InspectorTest.expectFalse(target.hasCommand("Timeline.setAutoCaptureEnabled"), `The ${description} should not have Timeline.setAutoCaptureEnabled.`);
50+
}
51+
InspectorTest.expectThat(WI.mainTarget.hasCommand("Timeline.setAutoCaptureEnabled"), "The page target should still have Timeline.setAutoCaptureEnabled.");
52+
}
53+
});
54+
55+
suite.addTestCase({
56+
name: "SiteIsolation.Timeline.FrameTargetRecords.Timers",
57+
description: "Timers in the main frame and the cross-origin iframe should be recorded by their own frame targets, and the recording should stop once both have stopped.",
58+
async test() {
59+
await FrameTimelineTest.startCapturing([mainFrameTarget, iframeTarget]);
60+
InspectorTest.pass("Both frame targets started tracking.");
61+
62+
let recording = WI.timelineManager.activeRecording;
63+
let mainFrameRecordPromise = FrameTimelineTest.awaitScriptRecord(mainFrameTarget, WI.ScriptTimelineRecord.EventType.TimerFired);
64+
let iframeRecordPromise = FrameTimelineTest.awaitScriptRecord(iframeTarget, WI.ScriptTimelineRecord.EventType.TimerFired);
65+
await FrameTimelineTest.evaluateInTarget(mainFrameTarget, "fireTimer()");
66+
await FrameTimelineTest.evaluateInTarget(iframeTarget, "fireTimer()");
67+
let mainFrameRecord = await mainFrameRecordPromise;
68+
InspectorTest.pass("Recorded a timer from the main frame target.");
69+
let iframeRecord = await iframeRecordPromise;
70+
InspectorTest.pass("Recorded a timer from the cross-origin iframe target.");
71+
72+
await FrameTimelineTest.stopCapturing();
73+
InspectorTest.pass("Capturing stopped.");
74+
75+
for (let target of [mainFrameTarget, iframeTarget])
76+
InspectorTest.expectThat(FrameTimelineTest.didReportTrackingEvent("capturingStopped", target), `The ${targetDescription(target)} should report that Timeline stopped.`);
77+
78+
expectRecordInRecording(mainFrameRecord, recording.startTime, recording.endTime);
79+
expectRecordInRecording(iframeRecord, recording.startTime, recording.endTime);
80+
expectNoScriptRecordsFromPageTarget(recording);
81+
82+
let scriptTimeline = recording.timelines.get(WI.TimelineRecord.Type.Script);
83+
for (let target of [mainFrameTarget, iframeTarget])
84+
InspectorTest.expectThat(scriptTimeline.targets.includes(target), `The Script timeline should include the ${targetDescription(target)}.`);
85+
}
86+
});
87+
88+
suite.addTestCase({
89+
name: "SiteIsolation.Timeline.FrameTargetRecords.EventDispatch",
90+
description: "An event dispatched in the cross-origin iframe should be recorded by its frame target.",
91+
async test() {
92+
await FrameTimelineTest.startCapturing([iframeTarget]);
93+
94+
let recording = WI.timelineManager.activeRecording;
95+
let recordPromise = FrameTimelineTest.awaitScriptRecord(iframeTarget, WI.ScriptTimelineRecord.EventType.EventDispatched);
96+
await FrameTimelineTest.evaluateInTarget(iframeTarget, "dispatchTestEvent()");
97+
let record = await recordPromise;
98+
InspectorTest.expectEqual(record.details, "test-event", "Recorded the dispatch of the test event from the cross-origin iframe target.");
99+
100+
await FrameTimelineTest.stopCapturing();
101+
InspectorTest.pass("Capturing stopped.");
102+
103+
expectRecordInRecording(record, recording.startTime, recording.endTime);
104+
expectNoScriptRecordsFromPageTarget(recording);
105+
}
106+
});
107+
108+
suite.addTestCase({
109+
name: "SiteIsolation.Timeline.FrameTargetRecords.NoFallbackToPageTarget",
110+
description: "While a frame target's Timeline domain is enabled but not tracking, its records should not go to the page target, even when the page target is tracking.",
111+
async test() {
112+
FrameTimelineTest.clearTrackingEvents();
113+
114+
let activePromise = FrameTimelineTest.awaitCapturingState(WI.TimelineManager.CapturingState.Active);
115+
await WI.mainTarget.TimelineAgent.start();
116+
await activePromise;
117+
InspectorTest.expectFalse(FrameTimelineTest.didReportTrackingEvent("capturingStarted", mainFrameTarget), "The main frame target should not be tracking.");
118+
119+
let recording = WI.timelineManager.activeRecording;
120+
await mainFrameTarget.RuntimeAgent.evaluate.invoke({expression: "new Promise((resolve) => setTimeout(() => { timerFired(); resolve(); }))", objectGroup: "test", awaitPromise: true});
121+
InspectorTest.pass("A timer fired in the main frame.");
122+
123+
// The page target sends any record before it reports that it stopped, so the check below sees them all.
124+
let inactivePromise = FrameTimelineTest.awaitCapturingState(WI.TimelineManager.CapturingState.Inactive);
125+
await WI.mainTarget.TimelineAgent.stop();
126+
await inactivePromise;
127+
InspectorTest.pass("Capturing stopped.");
128+
129+
let scriptTimeline = recording.timelines.get(WI.TimelineRecord.Type.Script);
130+
let timerRecords = scriptTimeline.records.filter((record) => record.eventType === WI.ScriptTimelineRecord.EventType.TimerFired);
131+
InspectorTest.expectFalse(timerRecords.some((record) => record.target === WI.mainTarget), "The page target should not record the main frame's timer.");
132+
}
133+
});
134+
135+
suite.runTestCasesAndFinish();
136+
}
137+
</script>
138+
139+
<body onload="runTest()">
140+
<p>Test that under Site Isolation, Timeline records are reported by the frame target of the frame that did the work, including a cross-origin iframe.</p>
141+
<iframe src="http://localhost:8000/site-isolation/inspector/timeline/resources/timeline-frame.html"></iframe>
142+
</body>

‎LayoutTests/http/tests/site-isolation/inspector/timeline/frame-target-timestamps.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
}
6868

6969
// Records an empty recording. Its start and end times come from the targets that have the Timeline domain: the
70-
// page target, whose clock is in the main frame's process, and the worker target.
70+
// page target, the frame targets and the worker target, whose clocks are in two different processes.
7171
async function recordTimeBounds() {
7272
let startTime = await startCapturing();
7373
let endTime = await stopCapturing();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Test that under Site Isolation, a Timeline recording can stop when it is waiting for a provisional frame target that is removed before it commits.
2+
3+
4+
== Running test suite: SiteIsolation.Timeline.ProvisionalFrameTargetRemovedDuringRecording
5+
-- Running test case: SiteIsolation.Timeline.ProvisionalFrameTargetRemovedDuringRecording
6+
PASS: Added a provisional frame target for the cross-origin load.
7+
PASS: Capturing should wait for the provisional frame target.
8+
PASS: The provisional frame target was removed.
9+
PASS: Capturing stopped.
10+

0 commit comments

Comments
 (0)