RESOLVED FIXED Bug 174739
Web Inspector: capture an async stack trace when web content calls addEventListener
https://bugs.webkit.org/show_bug.cgi?id=174739
Summary Web Inspector: capture an async stack trace when web content calls addEventLi...
Matt Baker
Reported 2017-07-21 18:25:27 PDT
Summary: Capture an async stack trace when web content calls addEventListener. - Add a new WebCore::AsynchronousCallType::EventListener enum value (this enum added in XXXX) - Instrument EventTarget - Add instrumentation end points to PageDebuggerAgent Note: Requires no new UI.
Attachments
Patch (21.70 KB, patch)
2017-07-21 19:20 PDT, Matt Baker
no flags
Patch (21.97 KB, patch)
2017-07-23 17:55 PDT, Matt Baker
no flags
Patch (22.96 KB, patch)
2017-07-26 18:30 PDT, Matt Baker
no flags
Patch (23.08 KB, patch)
2017-07-28 16:38 PDT, Matt Baker
no flags
Matt Baker
Comment 1 2017-07-21 19:20:16 PDT
Matt Baker
Comment 2 2017-07-21 19:21:16 PDT
Patch won't apply to TOT until the blocking issue lands.
Radar WebKit Bug Importer
Comment 3 2017-07-21 19:21:38 PDT
Radar WebKit Bug Importer
Comment 4 2017-07-21 19:22:08 PDT
Matt Baker
Comment 5 2017-07-23 17:55:23 PDT
Blaze Burg
Comment 6 2017-07-24 10:08:42 PDT
Comment on attachment 316244 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=316244&action=review r- because it needs rebasing. > Source/WebCore/dom/EventTarget.cpp:-72 > - return ensureEventTargetData().eventListenerMap.add(eventType, WTFMove(listener), { options.capture, options.passive, options.once }); This code seems kinda touchy for performance. Maybe you should check with Ryosuke to learn how to check whether this is a serious perf regression (adding hooks here on the hot path). > Source/WebCore/dom/EventTarget.cpp:72 > + bool listenerIsScriptDebuggable = listener->type() == EventListener::JSEventListenerType && !listener->wasCreatedFromMarkup(); Move this next to the use site. Also this name is wonky. What about listenerExecutesJavaScript or listenerCanBeDebugged? The !listener->wasCreatedFromMarkup() exclusion deserves some explanation here or in the ChangeLog. Why can't we debug that? > Source/WebCore/dom/EventTarget.cpp:76 > + if (listenerIsScriptDebuggable) { To minimize perf impact you should guard the extra work here (iterating the listeners) on InspectorInstrumentation::hasFrontends(). > Source/WebCore/dom/EventTarget.cpp:118 > + auto& listenersForType = eventListeners(eventType); To minimize perf impact you should guard the extra work here (iterating the listeners) on InspectorInstrumentation::hasFrontends(). > Source/WebCore/dom/EventTarget.cpp:120 > + if (registeredListener->callback() == listener && registeredListener->useCapture() == options.capture) { What is the purpose of this check for usesCapture? > Source/WebCore/inspector/InspectorInstrumentation.cpp:354 > +void InspectorInstrumentation::didRegisterEventListenerImpl(InstrumentingAgents& instrumentingAgents, const RegisteredEventListener& listener, ScriptExecutionContext& context) Applies throughout: please rename to add (so we have add/remove method pairs inside inspector). > Source/WebCore/inspector/PageDebuggerAgent.cpp:176 > + int listenerIdentifier = m_nextEventListenerIdentifier++; Please use an unsigned type for the identifier key. Adding a typedef would be nice too, so the HashMap type parameters are a little less vague. > Source/WebCore/inspector/PageDebuggerAgent.cpp:179 > + didScheduleAsyncCall(scriptState, static_cast<int>(AsynchronousCallType::EventListener), listenerIdentifier, listener.isOnce()); what!? don't cast away the enum type here. Do it inside the methods if they need to use the value for hashing or whatever. Please fix the existing call sites, this is not good. > Source/WebCore/inspector/PageDebuggerAgent.cpp:184 > + auto eventListenerIterator = m_registeredEventListeners.find(&listener); Nit: you can just do `auto it = ...`. There's only one iterator, naming it is not helpful. > Source/WebCore/inspector/PageDebuggerAgent.cpp:195 > + auto eventListenerIterator = m_registeredEventListeners.find(&listener); Ditto.
Matt Baker
Comment 7 2017-07-25 12:53:28 PDT
Comment on attachment 316244 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=316244&action=review ` >> Source/WebCore/dom/EventTarget.cpp:-72 >> - return ensureEventTargetData().eventListenerMap.add(eventType, WTFMove(listener), { options.capture, options.passive, options.once }); > > This code seems kinda touchy for performance. Maybe you should check with Ryosuke to learn how to check whether this is a serious perf regression (adding hooks here on the hot path). Will investigate. At the very least the call to "auto& listenersForType = ..." should be moved to InspectorInstrumentation and guarded on the presence of frontends. >> Source/WebCore/dom/EventTarget.cpp:72 >> + bool listenerIsScriptDebuggable = listener->type() == EventListener::JSEventListenerType && !listener->wasCreatedFromMarkup(); > > Move this next to the use site. Also this name is wonky. What about listenerExecutesJavaScript or listenerCanBeDebugged? > > The !listener->wasCreatedFromMarkup() exclusion deserves some explanation here or in the ChangeLog. Why can't we debug that? I agree this is a poor name. Maybe `listenerCreatedFromScript` would be more appropriate. The reason for ignoring listeners created from markup (<a onclick="didClick()">), is that no script call stack is associated with the event listener's registration. >> Source/WebCore/dom/EventTarget.cpp:76 >> + if (listenerIsScriptDebuggable) { > > To minimize perf impact you should guard the extra work here (iterating the listeners) on InspectorInstrumentation::hasFrontends(). EventTarget::eventListeners is public, so this step can be moved into InspectorInstrumentation. EventTarget will need to be passed to the instrumentation instead of ScriptExecutionContext (which can be retrieved from the former). >> Source/WebCore/dom/EventTarget.cpp:120 >> + if (registeredListener->callback() == listener && registeredListener->useCapture() == options.capture) { > > What is the purpose of this check for usesCapture? The same callback can be registered a second time so long as the `capture` option differs. See EventListenerMap::add.
Matt Baker
Comment 8 2017-07-25 13:51:22 PDT
Comment on attachment 316244 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=316244&action=review >> Source/WebCore/inspector/PageDebuggerAgent.cpp:176 >> + int listenerIdentifier = m_nextEventListenerIdentifier++; > > Please use an unsigned type for the identifier key. Adding a typedef would be nice too, so the HashMap type parameters are a little less vague. The identifier is signed to be consistent with requestAnimationFrame and DOMTimer identifiers handed out in WebCore.
Blaze Burg
Comment 9 2017-07-25 14:05:16 PDT
Comment on attachment 316244 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=316244&action=review >>> Source/WebCore/dom/EventTarget.cpp:72 >>> + bool listenerIsScriptDebuggable = listener->type() == EventListener::JSEventListenerType && !listener->wasCreatedFromMarkup(); >> >> Move this next to the use site. Also this name is wonky. What about listenerExecutesJavaScript or listenerCanBeDebugged? >> >> The !listener->wasCreatedFromMarkup() exclusion deserves some explanation here or in the ChangeLog. Why can't we debug that? > > I agree this is a poor name. Maybe `listenerCreatedFromScript` would be more appropriate. The reason for ignoring listeners created from markup (<a onclick="didClick()">), is that no script call stack is associated with the event listener's registration. Perhaps in a future patch, we could capture the call stack for the operation that creates the associated markup (i.e., the innerHTML setter, DOM mutation APIs, or loading HTML in a particular document). >>> Source/WebCore/dom/EventTarget.cpp:120 >>> + if (registeredListener->callback() == listener && registeredListener->useCapture() == options.capture) { >> >> What is the purpose of this check for usesCapture? > > The same callback can be registered a second time so long as the `capture` option differs. See EventListenerMap::add. oh, interesting.
Matt Baker
Comment 10 2017-07-26 18:30:47 PDT
Matt Baker
Comment 11 2017-07-26 18:34:09 PDT
EventTarget instrumentation changed to do no work when no Inspector frontends aren't present.
Matt Baker
Comment 12 2017-07-26 18:35:28 PDT
(In reply to Matt Baker from comment #11) > EventTarget instrumentation changed to do no work when no Inspector > frontends aren't present. >_< Only does work when Inspector frontends are present.
Blaze Burg
Comment 13 2017-07-28 10:34:57 PDT
Comment on attachment 316504 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=316504&action=review What happens if the page or frame containing the event listeners navigates? I don't see that we are clearing that anywhere. > Source/WebCore/dom/EventTarget.cpp:71 > + bool listenerCreatedFromScript = listener->type() == EventListener::JSEventListenerType && !listener->wasCreatedFromMarkup(); Move this local next to the use site. It isn't needed prior to the first early exit. > Source/WebCore/inspector/PageDebuggerAgent.cpp:169 > + auto& listener = eventListeners.last(); Please use the concrete type here. Auto is fine for the collection above.
Matt Baker
Comment 14 2017-07-28 16:07:28 PDT
Comment on attachment 316504 [details] Patch View in context: https://bugs.webkit.org/attachment.cgi?id=316504&action=review >> Source/WebCore/dom/EventTarget.cpp:71 >> + bool listenerCreatedFromScript = listener->type() == EventListener::JSEventListenerType && !listener->wasCreatedFromMarkup(); > > Move this local next to the use site. It isn't needed prior to the first early exit. This needs to come before we WTFMove the listener.
Matt Baker
Comment 15 2017-07-28 16:16:38 PDT
(In reply to Brian Burg from comment #13) > Comment on attachment 316504 [details] > Patch > > View in context: > https://bugs.webkit.org/attachment.cgi?id=316504&action=review > > What happens if the page or frame containing the event listeners navigates? > I don't see that we are clearing that anywhere. PageDebuggerAgent overrides didClearAsyncStackData, which is called when the global object is cleared. This is unnecessarily complex — PageDebuggerAgent can just clear its data in didClearMainFrameWindowObject: void PageDebuggerAgent::didClearMainFrameWindowObject() { didClearGlobalObject(); + m_registeredEventListeners.clear(); + m_nextEventListenerIdentifier = 1; }
Matt Baker
Comment 16 2017-07-28 16:23:23 PDT
(In reply to Matt Baker from comment #15) > (In reply to Brian Burg from comment #13) > > Comment on attachment 316504 [details] > > Patch > > > > View in context: > > https://bugs.webkit.org/attachment.cgi?id=316504&action=review > > > > What happens if the page or frame containing the event listeners navigates? > > I don't see that we are clearing that anywhere. > > PageDebuggerAgent overrides didClearAsyncStackData, which is called when the > global object is cleared. This is unnecessarily complex — PageDebuggerAgent > can just clear its data in didClearMainFrameWindowObject: > > void PageDebuggerAgent::didClearMainFrameWindowObject() > { > didClearGlobalObject(); > > + m_registeredEventListeners.clear(); > + m_nextEventListenerIdentifier = 1; > } Nevermind. The hook `didClearAsyncStackTraceData()` is needed, since there are multiple things in the InspectorDebuggerAgent that can cause async data top be cleared: - Window object cleared - Agent disabled - Change to async stack trace depth setting
Matt Baker
Comment 17 2017-07-28 16:38:29 PDT
Blaze Burg
Comment 18 2017-07-28 17:05:11 PDT
Comment on attachment 316685 [details] Patch r=me
WebKit Commit Bot
Comment 19 2017-07-28 17:33:16 PDT
Comment on attachment 316685 [details] Patch Clearing flags on attachment: 316685 Committed r220036: <http://trac.webkit.org/changeset/220036>
WebKit Commit Bot
Comment 20 2017-07-28 17:33:18 PDT
All reviewed patches have been landed. Closing bug.
Note You need to log in before you can comment on or make changes to this bug.