Source/WebCore: Event listener for active DOM object that is also DOM node can...

Source/WebCore: Event listener for active DOM object that is also DOM node can be garbage collected prematurely.
https://bugs.webkit.org/show_bug.cgi?id=70421

Patch by Eugene Nalimov <enal@google.com> on 2011-11-15
Reviewed by Adam Barth.

Problem demonstrated itself when HTMLAudioElement was changed to become active DOM object.
Before that there were no DOM objects that simultaneously were nodes and active objects.
DOM object could be held in one of 3 maps -- node map, active objects map, and all other
objects map, and HTMLAudioElement should be in 2 maps simultaneously. When it was in the
active DOM objects map only, its event listener could be garbage collected, because special
code that groups listeners with wrappers could handle only wrappers for objects in the node
map. If we put HTMLAudioElement into nodes map, it would not be active DOM node, and can be
garbage collected prematurely itself (see https://bugs.webkit.org/show_bug.cgi?id=66878).
Fix is to introduce 4th map -- active nodes map, and change the code accordingly.

Test: media/audio-garbage-collect.html

* bindings/scripts/CodeGeneratorV8.pm:
(GenerateNamedConstructorCallback):
(GetDomMapFunction):
* bindings/v8/DOMDataStore.cpp:
(WebCore::DOMDataStore::DOMDataStore):
(WebCore::DOMDataStore::getDOMWrapperMap):
(WebCore::DOMDataStore::weakNodeCallback):
* bindings/v8/DOMDataStore.h:
(WebCore::DOMDataStore::activeDomNodeMap):
* bindings/v8/ScopedDOMDataStore.cpp:
(WebCore::ScopedDOMDataStore::ScopedDOMDataStore):
(WebCore::ScopedDOMDataStore::~ScopedDOMDataStore):
* bindings/v8/StaticDOMDataStore.cpp:
(WebCore::StaticDOMDataStore::StaticDOMDataStore):
* bindings/v8/StaticDOMDataStore.h:
* bindings/v8/V8DOMMap.cpp:
(WebCore::getActiveDOMNodeMap):
(WebCore::removeAllDOMObjects):
(WebCore::visitActiveDOMNodes):
* bindings/v8/V8DOMMap.h:
* bindings/v8/V8DOMWrapper.cpp:
(WebCore::V8DOMWrapper::setJSWrapperForDOMNode):
(WebCore::V8DOMWrapper::getWrapperSlow):
* bindings/v8/V8GCController.cpp:
(WebCore::GCPrologueSpecialCase):
(WebCore::void):
(WebCore::Node):
(WebCore::GCPrologueVisitor::visitDOMWrapper):
(WebCore::V8GCController::gcPrologue):
(WebCore::GCEpilogueHelper::GCEpilogueSpecialCase):
(WebCore::GCEpilogueVisitor::visitDOMWrapper):
(WebCore::V8GCController::gcEpilogue):
* dom/Node.h:
(WebCore::Node::isActiveNode):
* html/HTMLAudioElement.h:
(WebCore::HTMLAudioElement::isActiveNode):

LayoutTests: Event listener for active DOM object that is also DOM node can be garbage collected prematurely.
https://bugs.webkit.org/show_bug.cgi?id=70421 and https://bugs.webkit.org/show_bug.cgi?id=66878

Patch by Eugene Nalimov <enal@google.com> on 2011-11-15
Reviewed by Adam Barth.

* media/audio-garbage-collect-expected.txt: Added.
* media/audio-garbage-collect.html: Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@100307 268f45cc-cd09-0410-ab3c-d52691b4dbfc
parent db57305e
2011-11-15 Eugene Nalimov <enal@google.com>
Event listener for active DOM object that is also DOM node can be garbage collected prematurely.
https://bugs.webkit.org/show_bug.cgi?id=70421 and https://bugs.webkit.org/show_bug.cgi?id=66878
Reviewed by Adam Barth.
* media/audio-garbage-collect-expected.txt: Added.
* media/audio-garbage-collect.html: Added.
2011-11-15 Robert Hogan <robert@webkit.org>
Mac results for r100177
Tests that we don't garbage collect playing audio object or event listener.
According to http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html,
"4.8.10.8 Playing the media resource",
"Media elements must not stop playing just because all references to them have been removed; only once a media element is in a state where no further audio could ever be played by that element may the element be garbage collected."
(see https://bugs.webkit.org/show_bug.cgi?id=66878, https://bugs.webkit.org/show_bug.cgi?id=70421, and http://crbug.com/62604 for more details).
PASS
<!DOCTYPE HTML>
<html>
<body>
<p>Tests that we don't garbage collect playing audio object or event listener.</p>
<p>According to http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html,<br />
"4.8.10.8 Playing the media resource",<br />
"Media elements must not stop playing just because all references to them have
been removed; only once a media element is in a state where no further audio
could ever be played by that element may the element be garbage collected."<br /><br />
(see https://bugs.webkit.org/show_bug.cgi?id=66878, https://bugs.webkit.org/show_bug.cgi?id=70421, and http://crbug.com/62604 for more details).</p>
<p id="result">
FAIL: Test either still running or stopped prematurely.
</p>
<script src=../resources/gc.js></script>
<script src=media-file.js></script>
<script src=video-test.js></script>
<script type="text/javascript">
var num_players = 4;
var play_times = 5;
function finish() {
document.getElementById("result").innerText = "PASS";
if (window.layoutTestController) {
layoutTestController.notifyDone();
}
}
function start() {
var num_played = 0;
var audioFile = findMediaFile("audio", "content/silence");
var a = new Audio(audioFile);
a.addEventListener('ended', function() {
num_played ++;
if (num_played < play_times) {
a.currentTime = a.duration - 0.35;
a.play();
if (num_played == play_times - 1) {
a = null;
gc();
}
} else {
num_players --;
if (num_players == 0)
start();
else
finish();
}
});
a.addEventListener('canplaythrough', function() {
a.currentTime = a.duration - 0.35;
a.play();
});
}
start();
</script>
</body>
</html>
2011-11-15 Eugene Nalimov <enal@google.com>
Event listener for active DOM object that is also DOM node can be garbage collected prematurely.
https://bugs.webkit.org/show_bug.cgi?id=70421
Reviewed by Adam Barth.
Problem demonstrated itself when HTMLAudioElement was changed to become active DOM object.
Before that there were no DOM objects that simultaneously were nodes and active objects.
DOM object could be held in one of 3 maps -- node map, active objects map, and all other
objects map, and HTMLAudioElement should be in 2 maps simultaneously. When it was in the
active DOM objects map only, its event listener could be garbage collected, because special
code that groups listeners with wrappers could handle only wrappers for objects in the node
map. If we put HTMLAudioElement into nodes map, it would not be active DOM node, and can be
garbage collected prematurely itself (see https://bugs.webkit.org/show_bug.cgi?id=66878).
Fix is to introduce 4th map -- active nodes map, and change the code accordingly.
Test: media/audio-garbage-collect.html
* bindings/scripts/CodeGeneratorV8.pm:
(GenerateNamedConstructorCallback):
(GetDomMapFunction):
* bindings/v8/DOMDataStore.cpp:
(WebCore::DOMDataStore::DOMDataStore):
(WebCore::DOMDataStore::getDOMWrapperMap):
(WebCore::DOMDataStore::weakNodeCallback):
* bindings/v8/DOMDataStore.h:
(WebCore::DOMDataStore::activeDomNodeMap):
* bindings/v8/ScopedDOMDataStore.cpp:
(WebCore::ScopedDOMDataStore::ScopedDOMDataStore):
(WebCore::ScopedDOMDataStore::~ScopedDOMDataStore):
* bindings/v8/StaticDOMDataStore.cpp:
(WebCore::StaticDOMDataStore::StaticDOMDataStore):
* bindings/v8/StaticDOMDataStore.h:
* bindings/v8/V8DOMMap.cpp:
(WebCore::getActiveDOMNodeMap):
(WebCore::removeAllDOMObjects):
(WebCore::visitActiveDOMNodes):
* bindings/v8/V8DOMMap.h:
* bindings/v8/V8DOMWrapper.cpp:
(WebCore::V8DOMWrapper::setJSWrapperForDOMNode):
(WebCore::V8DOMWrapper::getWrapperSlow):
* bindings/v8/V8GCController.cpp:
(WebCore::GCPrologueSpecialCase):
(WebCore::void):
(WebCore::Node):
(WebCore::GCPrologueVisitor::visitDOMWrapper):
(WebCore::V8GCController::gcPrologue):
(WebCore::GCEpilogueHelper::GCEpilogueSpecialCase):
(WebCore::GCEpilogueVisitor::visitDOMWrapper):
(WebCore::V8GCController::gcEpilogue):
* dom/Node.h:
(WebCore::Node::isActiveNode):
* html/HTMLAudioElement.h:
(WebCore::HTMLAudioElement::isActiveNode):
2011-11-15 David Kilzer <ddkilzer@apple.com>
Remove useless const modifier from KURL::init
......
......@@ -1777,11 +1777,12 @@ END
}
my $DOMObject = "DOMObject";
# A DOMObject that is an ActiveDOMObject and also a DOMNode should be treated as an ActiveDOMObject.
if ($dataNode->extendedAttributes->{"ActiveDOMObject"}) {
$DOMObject = "ActiveDOMObject";
} elsif (IsNodeSubType($dataNode)) {
# A DOMObject that is an ActiveDOMObject and also a DOMNode should be treated as an DOMNode here.
# setJSWrapperForDOMNode() will look if node is active and choose correct map to add node to.
if (IsNodeSubType($dataNode)) {
$DOMObject = "DOMNode";
} elsif ($dataNode->extendedAttributes->{"ActiveDOMObject"}) {
$DOMObject = "ActiveDOMObject";
}
push(@implContent, <<END);
......@@ -3105,6 +3106,7 @@ sub GetDomMapFunction
my $dataNode = shift;
my $type = shift;
return "getDOMSVGElementInstanceMap()" if $type eq "SVGElementInstance";
return "getActiveDOMNodeMap()" if (IsNodeSubType($dataNode) && $dataNode->extendedAttributes->{"ActiveDOMObject"});
return "getDOMNodeMap()" if (IsNodeSubType($dataNode));
return "getActiveDOMObjectMap()" if $dataNode->extendedAttributes->{"ActiveDOMObject"};
return "getDOMObjectMap()";
......
......@@ -86,6 +86,7 @@ namespace WebCore {
DOMDataStore::DOMDataStore()
: m_domNodeMap(0)
, m_activeDomNodeMap(0)
, m_domObjectMap(0)
, m_activeDomObjectMap(0)
#if ENABLE(SVG)
......@@ -108,6 +109,8 @@ void* DOMDataStore::getDOMWrapperMap(DOMWrapperMapType type)
switch (type) {
case DOMNodeMap:
return m_domNodeMap;
case ActiveDOMNodeMap:
return m_activeDomNodeMap;
case DOMObjectMap:
return m_domObjectMap;
case ActiveDOMObjectMap:
......@@ -149,7 +152,8 @@ void DOMDataStore::weakNodeCallback(v8::Persistent<v8::Value> value, void* domOb
DOMDataList& list = DOMDataStore::allStores();
for (size_t i = 0; i < list.size(); ++i) {
DOMDataStore* store = list[i];
if (store->domNodeMap().removeIfPresent(node, v8Object)) {
DOMNodeMapping& nodeMap = node->isActiveNode() ? store->activeDomNodeMap() : store->domNodeMap();
if (nodeMap.removeIfPresent(node, v8Object)) {
node->deref(); // Nobody overrides Node::deref so it's safe
return; // There might be at most one wrapper for the node in world's maps
}
......
......@@ -65,6 +65,7 @@ namespace WebCore {
public:
enum DOMWrapperMapType {
DOMNodeMap,
ActiveDOMNodeMap,
DOMObjectMap,
ActiveDOMObjectMap,
#if ENABLE(SVG)
......@@ -81,6 +82,7 @@ namespace WebCore {
void* getDOMWrapperMap(DOMWrapperMapType);
DOMNodeMapping& domNodeMap() { return *m_domNodeMap; }
DOMNodeMapping& activeDomNodeMap() { return *m_activeDomNodeMap; }
DOMWrapperMap<void>& domObjectMap() { return *m_domObjectMap; }
DOMWrapperMap<void>& activeDomObjectMap() { return *m_activeDomObjectMap; }
#if ENABLE(SVG)
......@@ -89,15 +91,16 @@ namespace WebCore {
// Need by V8GCController.
static void weakActiveDOMObjectCallback(v8::Persistent<v8::Value> v8Object, void* domObject);
static void weakNodeCallback(v8::Persistent<v8::Value> v8Object, void* domObject);
protected:
static void weakNodeCallback(v8::Persistent<v8::Value> v8Object, void* domObject);
static void weakDOMObjectCallback(v8::Persistent<v8::Value> v8Object, void* domObject);
#if ENABLE(SVG)
static void weakSVGElementInstanceCallback(v8::Persistent<v8::Value> v8Object, void* domObject);
#endif
DOMNodeMapping* m_domNodeMap;
DOMNodeMapping* m_activeDomNodeMap;
DOMWrapperMap<void>* m_domObjectMap;
DOMWrapperMap<void>* m_activeDomObjectMap;
#if ENABLE(SVG)
......
......@@ -37,6 +37,7 @@ ScopedDOMDataStore::ScopedDOMDataStore()
: DOMDataStore()
{
m_domNodeMap = new DOMWrapperMap<Node>(&DOMDataStore::weakNodeCallback);
m_activeDomNodeMap = new DOMWrapperMap<Node>(&DOMDataStore::weakNodeCallback);
m_domObjectMap = new DOMWrapperMap<void>(&DOMDataStore::weakDOMObjectCallback);
m_activeDomObjectMap = new DOMWrapperMap<void>(&DOMDataStore::weakActiveDOMObjectCallback);
#if ENABLE(SVG)
......@@ -47,6 +48,7 @@ ScopedDOMDataStore::ScopedDOMDataStore()
ScopedDOMDataStore::~ScopedDOMDataStore()
{
delete m_domNodeMap;
delete m_activeDomNodeMap;
delete m_domObjectMap;
delete m_activeDomObjectMap;
#if ENABLE(SVG)
......
......@@ -37,6 +37,7 @@ namespace WebCore {
StaticDOMDataStore::StaticDOMDataStore()
: DOMDataStore()
, m_staticDomNodeMap(&DOMDataStore::weakNodeCallback)
, m_staticActiveDomNodeMap(&DOMDataStore::weakNodeCallback)
, m_staticDomObjectMap(&DOMDataStore::weakDOMObjectCallback)
, m_staticActiveDomObjectMap(&DOMDataStore::weakActiveDOMObjectCallback)
#if ENABLE(SVG)
......@@ -44,6 +45,7 @@ StaticDOMDataStore::StaticDOMDataStore()
#endif
{
m_domNodeMap = &m_staticDomNodeMap;
m_activeDomNodeMap = &m_staticActiveDomNodeMap;
m_domObjectMap = &m_staticDomObjectMap;
m_activeDomObjectMap = &m_staticActiveDomObjectMap;
#if ENABLE(SVG)
......
......@@ -51,6 +51,7 @@ public:
private:
IntrusiveDOMWrapperMap m_staticDomNodeMap;
IntrusiveDOMWrapperMap m_staticActiveDomNodeMap;
DOMWrapperMap<void> m_staticDomObjectMap;
DOMWrapperMap<void> m_staticActiveDomObjectMap;
#if ENABLE(SVG)
......
......@@ -64,6 +64,11 @@ DOMNodeMapping& getDOMNodeMap()
return getDOMDataStore().domNodeMap();
}
DOMNodeMapping& getActiveDOMNodeMap()
{
return getDOMDataStore().activeDomNodeMap();
}
DOMWrapperMap<void>& getDOMObjectMap()
{
return getDOMDataStore().domObjectMap();
......@@ -94,6 +99,9 @@ void removeAllDOMObjects()
// Remove all DOM nodes.
DOMData::removeObjectsFromWrapperMap<Node>(&store, store.domNodeMap());
// Remove all active DOM nodes.
DOMData::removeObjectsFromWrapperMap<Node>(&store, store.activeDomNodeMap());
#if ENABLE(SVG)
// Remove all SVG element instances in the wrapper map.
DOMData::removeObjectsFromWrapperMap<SVGElementInstance>(&store, store.domSvgElementInstanceMap());
......@@ -119,6 +127,18 @@ void visitDOMNodes(DOMWrapperMap<Node>::Visitor* visitor)
}
}
void visitActiveDOMNodes(DOMWrapperMap<Node>::Visitor* visitor)
{
v8::HandleScope scope;
DOMDataList& list = DOMDataStore::allStores();
for (size_t i = 0; i < list.size(); ++i) {
DOMDataStore* store = list[i];
store->activeDomNodeMap().visit(store, visitor);
}
}
void visitDOMObjects(DOMWrapperMap<void>::Visitor* visitor)
{
v8::HandleScope scope;
......
......@@ -158,7 +158,9 @@ namespace WebCore {
// A map from DOM node to its JS wrapper.
DOMNodeMapping& getDOMNodeMap();
DOMNodeMapping& getActiveDOMNodeMap();
void visitDOMNodes(DOMWrapperMap<Node>::Visitor*);
void visitActiveDOMNodes(DOMWrapperMap<Node>::Visitor*);
// A map from a DOM object (non-node) to its JS wrapper. This map does not contain the DOM objects which can have pending activity (active dom objects).
DOMWrapperMap<void>& getDOMObjectMap();
......
......@@ -92,7 +92,10 @@ void V8DOMWrapper::setJSWrapperForActiveDOMObject(void* object, v8::Persistent<v
void V8DOMWrapper::setJSWrapperForDOMNode(Node* node, v8::Persistent<v8::Object> wrapper)
{
ASSERT(V8DOMWrapper::maybeDOMWrapper(wrapper));
getDOMNodeMap().set(node, wrapper);
if (node->isActiveNode())
getActiveDOMNodeMap().set(node, wrapper);
else
getDOMNodeMap().set(node, wrapper);
}
v8::Local<v8::Function> V8DOMWrapper::getConstructor(WrapperTypeInfo* type, v8::Handle<v8::Value> objectPrototype)
......@@ -295,7 +298,8 @@ v8::Handle<v8::Object> V8DOMWrapper::getWrapperSlow(Node* node)
return v8::Handle<v8::Object>();
return *wrapper;
}
DOMNodeMapping& domNodeMap = context->world()->domDataStore()->domNodeMap();
DOMDataStore* store = context->world()->domDataStore();
DOMNodeMapping& domNodeMap = node->isActiveNode() ? store->activeDomNodeMap() : store->domNodeMap();
return domNodeMap.get(node);
}
......
......@@ -143,12 +143,10 @@ public:
#endif // NDEBUG
class GCPrologueVisitor : public DOMWrapperMap<void>::Visitor {
class SpecialCasePrologueObjectHandler {
public:
void visitDOMWrapper(DOMDataStore* store, void* object, v8::Persistent<v8::Object> wrapper)
static bool process(void* object, v8::Persistent<v8::Object> wrapper, WrapperTypeInfo* typeInfo)
{
WrapperTypeInfo* typeInfo = V8DOMWrapper::domWrapperType(wrapper);
// Additional handling of message port ensuring that entangled ports also
// have their wrappers entangled. This should ideally be handled when the
// ports are actually entangled in MessagePort::entangle, but to avoid
......@@ -161,7 +159,31 @@ public:
MessagePort* port1 = static_cast<MessagePort*>(object);
if (port1->isEntangled() || port1->hasPendingActivity())
wrapper.ClearWeak();
} else {
return true;
}
return false;
}
};
class SpecialCasePrologueNodeHandler {
public:
static bool process(Node* object, v8::Persistent<v8::Object> wrapper, WrapperTypeInfo* typeInfo)
{
UNUSED_PARAM(object);
UNUSED_PARAM(wrapper);
UNUSED_PARAM(typeInfo);
return false;
}
};
template<typename T, typename S>
class GCPrologueVisitor : public DOMWrapperMap<T>::Visitor {
public:
void visitDOMWrapper(DOMDataStore* store, T* object, v8::Persistent<v8::Object> wrapper)
{
WrapperTypeInfo* typeInfo = V8DOMWrapper::domWrapperType(wrapper);
if (!S::process(object, wrapper, typeInfo)) {
ActiveDOMObject* activeDOMObject = typeInfo->toActiveDOMObject(wrapper);
if (activeDOMObject && activeDOMObject->hasPendingActivity())
wrapper.ClearWeak();
......@@ -373,12 +395,15 @@ void V8GCController::gcPrologue()
// Run through all objects with possible pending activity making their
// wrappers non weak if there is pending activity.
GCPrologueVisitor prologueVisitor;
visitActiveDOMObjects(&prologueVisitor);
GCPrologueVisitor<void, SpecialCasePrologueObjectHandler> prologueObjectVisitor;
visitActiveDOMObjects(&prologueObjectVisitor);
GCPrologueVisitor<Node, SpecialCasePrologueNodeHandler> prologueNodeVisitor;
visitActiveDOMNodes(&prologueNodeVisitor);
// Create object groups.
GrouperVisitor grouperVisitor;
visitDOMNodes(&grouperVisitor);
visitActiveDOMNodes(&grouperVisitor);
visitDOMObjects(&grouperVisitor);
grouperVisitor.applyGrouping();
......@@ -387,11 +412,10 @@ void V8GCController::gcPrologue()
data->stringCache()->clearOnGC();
}
class GCEpilogueVisitor : public DOMWrapperMap<void>::Visitor {
class SpecialCaseEpilogueObjectHandler {
public:
void visitDOMWrapper(DOMDataStore* store, void* object, v8::Persistent<v8::Object> wrapper)
static bool process(void* object, v8::Persistent<v8::Object> wrapper, WrapperTypeInfo* typeInfo)
{
WrapperTypeInfo* typeInfo = V8DOMWrapper::domWrapperType(wrapper);
if (V8MessagePort::info.equals(typeInfo)) {
MessagePort* port1 = static_cast<MessagePort*>(object);
// We marked this port as reachable in GCPrologueVisitor. Undo this now since the
......@@ -399,7 +423,30 @@ public:
// GCPrologueVisitor expects to see all handles marked as weak).
if ((!wrapper.IsWeak() && !wrapper.IsNearDeath()) || port1->hasPendingActivity())
wrapper.MakeWeak(port1, &DOMDataStore::weakActiveDOMObjectCallback);
} else {
return true;
}
return false;
}
};
class SpecialCaseEpilogueNodeHandler {
public:
static bool process(Node* object, v8::Persistent<v8::Object> wrapper, WrapperTypeInfo* typeInfo)
{
UNUSED_PARAM(object);
UNUSED_PARAM(wrapper);
UNUSED_PARAM(typeInfo);
return false;
}
};
template<typename T, typename S, v8::WeakReferenceCallback callback>
class GCEpilogueVisitor : public DOMWrapperMap<T>::Visitor {
public:
void visitDOMWrapper(DOMDataStore* store, T* object, v8::Persistent<v8::Object> wrapper)
{
WrapperTypeInfo* typeInfo = V8DOMWrapper::domWrapperType(wrapper);
if (!S::process(object, wrapper, typeInfo)) {
ActiveDOMObject* activeDOMObject = typeInfo->toActiveDOMObject(wrapper);
if (activeDOMObject && activeDOMObject->hasPendingActivity()) {
ASSERT(!wrapper.IsWeak());
......@@ -408,7 +455,7 @@ public:
// may be a different pointer (in case ActiveDOMObject is not
// the main base class of the object's class) and pointer
// identity is required by DOM map functions.
wrapper.MakeWeak(object, &DOMDataStore::weakActiveDOMObjectCallback);
wrapper.MakeWeak(object, callback);
}
}
}
......@@ -444,8 +491,10 @@ void V8GCController::gcEpilogue()
// Run through all objects with pending activity making their wrappers weak
// again.
GCEpilogueVisitor epilogueVisitor;
visitActiveDOMObjects(&epilogueVisitor);
GCEpilogueVisitor<void, SpecialCaseEpilogueObjectHandler, &DOMDataStore::weakActiveDOMObjectCallback> epilogueObjectVisitor;
visitActiveDOMObjects(&epilogueObjectVisitor);
GCEpilogueVisitor<Node, SpecialCaseEpilogueNodeHandler, &DOMDataStore::weakNodeCallback> epilogueNodeVisitor;
visitActiveDOMNodes(&epilogueNodeVisitor);
workingSetEstimateMB = getActualMemoryUsageInMB();
......
......@@ -190,6 +190,8 @@ public:
Node* lastDescendant() const;
Node* firstDescendant() const;
virtual bool isActiveNode() const { return false; }
// Other methods (not part of DOM)
......
......@@ -42,6 +42,8 @@ public:
virtual bool hasPendingActivity() const { return isPlaying() || HTMLMediaElement::hasPendingActivity(); }
virtual bool isActiveNode() const { return true; }
private:
HTMLAudioElement(const QualifiedName&, Document*);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment