Bug 1378201 - Improve the performance of TableRowsCollection, r=ehsan

MozReview-Commit-ID: 4joB73SXNGA
This commit is contained in:
Michael Layzell
2017-07-06 16:58:06 -04:00
parent d2ca0d77d0
commit 610e99abc3

View File

@@ -24,8 +24,9 @@ namespace dom {
* This class provides a late-bound collection of rows in a table.
* mParent is NOT ref-counted to avoid circular references
*/
class TableRowsCollection final : public nsIHTMLCollection,
public nsWrapperCache
class TableRowsCollection final : public nsIHTMLCollection
, public nsStubMutationObserver
, public nsWrapperCache
{
public:
explicit TableRowsCollection(HTMLTableElement* aParent);
@@ -33,6 +34,11 @@ public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_NSIDOMHTMLCOLLECTION
NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED
virtual Element* GetElementAt(uint32_t aIndex) override;
virtual nsINode* GetParentObject() override
{
@@ -45,14 +51,27 @@ public:
NS_IMETHOD ParentDestroyed();
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(TableRowsCollection)
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS(TableRowsCollection, nsIHTMLCollection)
// nsWrapperCache
using nsWrapperCache::GetWrapperPreserveColor;
using nsWrapperCache::PreserveWrapper;
virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
protected:
virtual ~TableRowsCollection();
// Unregister ourselves as a mutation observer, and clear our internal state.
void CleanUp();
void LastRelease()
{
CleanUp();
}
virtual ~TableRowsCollection()
{
// we do NOT have a ref-counted reference to mParent, so do NOT
// release it! this is to avoid circular references. The
// instantiator who provided mParent is responsible for managing our
// reference for us.
CleanUp();
}
virtual JSObject* GetWrapperPreserveColorInternal() override
{
@@ -63,22 +82,126 @@ protected:
nsWrapperCache::PreserveWrapper(aScriptObjectHolder);
}
// Those rows that are not in table sections
// Ensure that HTMLTableElement is in a valid state. This must be called
// before inspecting the mRows object.
void EnsureInitialized();
// Checks if the passed-in container is interesting for the purposes of
// invalidation due to a mutation observer.
bool InterestingContainer(nsIContent* aContainer);
// Check if the passed-in nsIContent is a <tr> within the section defined by
// `aSection`. The root of the table is considered to be part of the `<tbody>`
// section.
bool IsAppropriateRow(nsIAtom* aSection, nsIContent* aContent);
// Scan backwards starting from `aCurrent` in the table, looking for the
// previous row in the table which is within the section `aSection`.
nsIContent* PreviousRow(nsIAtom* aSection, nsIContent* aCurrent);
// Handle the insertion of the child `aChild` into the container `aContainer`
// within the tree. The container must be an `InterestingContainer`. This
// method updates the mRows, mBodyStart, and mFootStart member variables.
//
// HandleInsert returns an integer which can be passed to the next call of the
// method in a loop inserting children into the same container. This will
// optimize subsequent insertions to require less work. This can either be -1,
// in which case we don't know where to insert the next row, and When passed
// to HandleInsert, it will use `PreviousRow` to locate the index to insert.
// Or, it can be an index to insert the next <tr> in the same container at.
int32_t HandleInsert(nsIContent* aContainer,
nsIContent* aChild,
int32_t aIndexGuess = -1);
// The HTMLTableElement which this TableRowsCollection tracks the rows for.
HTMLTableElement* mParent;
// The current state of the TableRowsCollection. mBodyStart and mFootStart are
// indices into mRows which represent the location of the first row in the
// body or foot section. If there are no rows in a section, the index points
// at the location where the first element in that section would be inserted.
nsTArray<nsCOMPtr<nsIContent>> mRows;
uint32_t mBodyStart;
uint32_t mFootStart;
bool mInitialized;
};
TableRowsCollection::TableRowsCollection(HTMLTableElement *aParent)
: mParent(aParent)
, mBodyStart(0)
, mFootStart(0)
, mInitialized(false)
{
MOZ_ASSERT(mParent);
}
TableRowsCollection::~TableRowsCollection()
void
TableRowsCollection::EnsureInitialized()
{
// we do NOT have a ref-counted reference to mParent, so do NOT
// release it! this is to avoid circular references. The
// instantiator who provided mParent is responsible for managing our
// reference for us.
if (mInitialized) {
return;
}
mInitialized = true;
// Initialize mRows as the TableRowsCollection is created. The mutation
// observer should keep it up to date.
//
// It should be extremely unlikely that anyone creates a TableRowsCollection
// without calling a method on it, so lazily performing this initialization
// seems unnecessary.
AutoTArray<nsCOMPtr<nsIContent>, 32> body;
AutoTArray<nsCOMPtr<nsIContent>, 32> foot;
mRows.Clear();
auto addRowChildren = [&] (nsTArray<nsCOMPtr<nsIContent>>& aArray, nsIContent* aNode) {
for (nsIContent* inner = aNode->nsINode::GetFirstChild();
inner; inner = inner->GetNextSibling()) {
if (inner->IsHTMLElement(nsGkAtoms::tr)) {
aArray.AppendElement(inner);
}
}
};
for (nsIContent* node = mParent->nsINode::GetFirstChild();
node; node = node->GetNextSibling()) {
if (node->IsHTMLElement(nsGkAtoms::thead)) {
addRowChildren(mRows, node);
} else if (node->IsHTMLElement(nsGkAtoms::tbody)) {
addRowChildren(body, node);
} else if (node->IsHTMLElement(nsGkAtoms::tfoot)) {
addRowChildren(foot, node);
} else if (node->IsHTMLElement(nsGkAtoms::tr)) {
body.AppendElement(node);
}
}
mBodyStart = mRows.Length();
mRows.AppendElements(Move(body));
mFootStart = mRows.Length();
mRows.AppendElements(Move(foot));
mParent->AddMutationObserver(this);
}
void
TableRowsCollection::CleanUp()
{
// Unregister ourselves as a mutation observer.
if (mInitialized && mParent) {
mParent->RemoveMutationObserver(this);
}
// Clean up all of our internal state and make it empty in case someone looks
// at us.
mRows.Clear();
mBodyStart = 0;
mFootStart = 0;
// We set mInitialized to true in case someone still has a reference to us, as
// we don't need to try to initialize first.
mInitialized = true;
mParent = nullptr;
}
JSObject*
@@ -87,136 +210,33 @@ TableRowsCollection::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProt
return HTMLCollectionBinding::Wrap(aCx, this, aGivenProto);
}
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(TableRowsCollection)
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TableRowsCollection, mRows)
NS_IMPL_CYCLE_COLLECTING_ADDREF(TableRowsCollection)
NS_IMPL_CYCLE_COLLECTING_RELEASE(TableRowsCollection)
NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(TableRowsCollection,
LastRelease())
NS_INTERFACE_TABLE_HEAD(TableRowsCollection)
NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY
NS_INTERFACE_TABLE(TableRowsCollection, nsIHTMLCollection,
nsIDOMHTMLCollection)
nsIDOMHTMLCollection, nsIMutationObserver)
NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(TableRowsCollection)
NS_INTERFACE_MAP_END
// Macro that can be used to avoid copy/pasting code to iterate over the
// rowgroups. _code should be the code to execute for each rowgroup. The
// rowgroup's rows will be in the nsIDOMHTMLCollection* named "rows".
// _trCode should be the code to execute for each tr row. Note that
// this may be null at any time. This macro assumes an nsresult named
// |rv| is in scope.
#define DO_FOR_EACH_BY_ORDER(_code, _trCode) \
do { \
if (mParent) { \
HTMLTableSectionElement* rowGroup; \
nsIHTMLCollection* rows; \
/* THead */ \
for (nsIContent* _node = mParent->nsINode::GetFirstChild(); \
_node; _node = _node->GetNextSibling()) { \
if (_node->IsHTMLElement(nsGkAtoms::thead)) { \
rowGroup = static_cast<HTMLTableSectionElement*>(_node);\
rows = rowGroup->Rows(); \
do { /* gives scoping */ \
_code \
} while (0); \
} \
} \
/* TBodies */ \
for (nsIContent* _node = mParent->nsINode::GetFirstChild(); \
_node; _node = _node->GetNextSibling()) { \
if (_node->IsHTMLElement(nsGkAtoms::tr)) { \
do { \
_trCode \
} while (0); \
} else if (_node->IsHTMLElement(nsGkAtoms::tbody)) { \
rowGroup = static_cast<HTMLTableSectionElement*>(_node); \
rows = rowGroup->Rows(); \
do { /* gives scoping */ \
_code \
} while (0); \
} \
} \
/* TFoot */ \
for (nsIContent* _node = mParent->nsINode::GetFirstChild(); \
_node; _node = _node->GetNextSibling()) { \
if (_node->IsHTMLElement(nsGkAtoms::tfoot)) { \
rowGroup = static_cast<HTMLTableSectionElement*>(_node);\
rows = rowGroup->Rows(); \
do { /* gives scoping */ \
_code \
} while (0); \
} \
} \
} \
} while (0)
static uint32_t
CountRowsInRowGroup(nsIDOMHTMLCollection* rows)
{
uint32_t length = 0;
if (rows) {
rows->GetLength(&length);
}
return length;
}
// we re-count every call. A better implementation would be to set
// ourselves up as an observer of contentAppended, contentInserted,
// and contentDeleted
NS_IMETHODIMP
TableRowsCollection::GetLength(uint32_t* aLength)
{
*aLength=0;
DO_FOR_EACH_BY_ORDER({
*aLength += CountRowsInRowGroup(rows);
}, {
(*aLength)++;
});
EnsureInitialized();
*aLength = mRows.Length();
return NS_OK;
}
// Returns the item at index aIndex if available. If null is returned,
// then aCount will be set to the number of rows in this row collection.
// Otherwise, the value of aCount is undefined.
static Element*
GetItemOrCountInRowGroup(nsIDOMHTMLCollection* rows,
uint32_t aIndex, uint32_t* aCount)
{
*aCount = 0;
if (rows) {
rows->GetLength(aCount);
if (aIndex < *aCount) {
nsIHTMLCollection* list = static_cast<nsIHTMLCollection*>(rows);
return list->GetElementAt(aIndex);
}
}
return nullptr;
}
Element*
TableRowsCollection::GetElementAt(uint32_t aIndex)
{
DO_FOR_EACH_BY_ORDER({
uint32_t count;
Element* node = GetItemOrCountInRowGroup(rows, aIndex, &count);
if (node) {
return node;
}
NS_ASSERTION(count <= aIndex, "GetItemOrCountInRowGroup screwed up");
aIndex -= count;
},{
if (aIndex == 0) {
return _node->AsElement();
}
aIndex--;
});
EnsureInitialized();
if (aIndex < mRows.Length()) {
return mRows[aIndex]->AsElement();
}
return nullptr;
}
@@ -236,23 +256,20 @@ TableRowsCollection::Item(uint32_t aIndex, nsIDOMNode** aReturn)
Element*
TableRowsCollection::GetFirstNamedElement(const nsAString& aName, bool& aFound)
{
EnsureInitialized();
aFound = false;
nsCOMPtr<nsIAtom> nameAtom = NS_Atomize(aName);
NS_ENSURE_TRUE(nameAtom, nullptr);
DO_FOR_EACH_BY_ORDER({
Element* item = rows->NamedGetter(aName, aFound);
if (aFound) {
return item;
}
}, {
if (_node->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
nameAtom, eCaseMatters) ||
_node->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id,
nameAtom, eCaseMatters)) {
for (auto& node : mRows) {
if (node->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name,
nameAtom, eCaseMatters) ||
node->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id,
nameAtom, eCaseMatters)) {
aFound = true;
return _node->AsElement();
return node->AsElement();
}
});
}
return nullptr;
}
@@ -260,20 +277,10 @@ TableRowsCollection::GetFirstNamedElement(const nsAString& aName, bool& aFound)
void
TableRowsCollection::GetSupportedNames(nsTArray<nsString>& aNames)
{
DO_FOR_EACH_BY_ORDER({
nsTArray<nsString> names;
nsCOMPtr<nsIHTMLCollection> coll = do_QueryInterface(rows);
if (coll) {
coll->GetSupportedNames(names);
for (uint32_t i = 0; i < names.Length(); ++i) {
if (!aNames.Contains(names[i])) {
aNames.AppendElement(names[i]);
}
}
}
}, {
if (_node->HasID()) {
nsIAtom* idAtom = _node->GetID();
EnsureInitialized();
for (auto& node : mRows) {
if (node->HasID()) {
nsIAtom* idAtom = node->GetID();
MOZ_ASSERT(idAtom != nsGkAtoms::_empty,
"Empty ids don't get atomized");
nsDependentAtomString idStr(idAtom);
@@ -282,7 +289,7 @@ TableRowsCollection::GetSupportedNames(nsTArray<nsString>& aNames)
}
}
nsGenericHTMLElement* el = nsGenericHTMLElement::FromContent(_node);
nsGenericHTMLElement* el = nsGenericHTMLElement::FromContent(node);
if (el) {
const nsAttrValue* val = el->GetParsedAttr(nsGkAtoms::name);
if (val && val->Type() == nsAttrValue::eAtom) {
@@ -295,7 +302,7 @@ TableRowsCollection::GetSupportedNames(nsTArray<nsString>& aNames)
}
}
}
});
}
}
@@ -317,12 +324,257 @@ TableRowsCollection::NamedItem(const nsAString& aName,
NS_IMETHODIMP
TableRowsCollection::ParentDestroyed()
{
// see comment in destructor, do NOT release mParent!
mParent = nullptr;
CleanUp();
return NS_OK;
}
bool
TableRowsCollection::InterestingContainer(nsIContent* aContainer)
{
return mParent && aContainer &&
(aContainer == mParent ||
(aContainer->GetParent() == mParent &&
aContainer->IsAnyOfHTMLElements(nsGkAtoms::thead,
nsGkAtoms::tbody,
nsGkAtoms::tfoot)));
}
bool
TableRowsCollection::IsAppropriateRow(nsIAtom* aSection, nsIContent* aContent)
{
if (!aContent->IsHTMLElement(nsGkAtoms::tr)) {
return false;
}
// If it's in the root, then we consider it to be in a tbody.
nsIContent* parent = aContent->GetParent();
if (aSection == nsGkAtoms::tbody && parent == mParent) {
return true;
}
return parent->IsHTMLElement(aSection);
}
nsIContent*
TableRowsCollection::PreviousRow(nsIAtom* aSection, nsIContent* aCurrent)
{
// Keep going backwards until we've found a `tr` element. We want to always
// run at least once, as we don't want to find ourselves.
//
// Each spin of the loop we step backwards one element. If we're at the top of
// a section, we step out of it into the root, and if we step onto a section
// matching `aSection`, we step into it. We keep spinning the loop until
// either we reach the first element in mParent, or find a <tr> in an
// appropriate section.
nsIContent* prev = aCurrent;
do {
nsIContent* parent = prev->GetParent();
prev = prev->GetPreviousSibling();
// Ascend out of any sections we're currently in, if we've run out of
// elements.
if (!prev && parent != mParent) {
prev = parent->GetPreviousSibling();
}
// Descend into a section if we stepped onto one.
if (prev && prev->GetParent() == mParent && prev->IsHTMLElement(aSection)) {
prev = prev->GetLastChild();
}
} while (prev && !IsAppropriateRow(aSection, prev));
return prev;
}
int32_t
TableRowsCollection::HandleInsert(nsIContent* aContainer,
nsIContent* aChild,
int32_t aIndexGuess)
{
if (!nsContentUtils::IsInSameAnonymousTree(mParent, aChild)) {
return aIndexGuess; // Nothing inserted, guess hasn't changed.
}
// If we're adding a section to the root, add each of the rows in that section
// individually.
if (aContainer == mParent &&
aChild->IsAnyOfHTMLElements(nsGkAtoms::thead,
nsGkAtoms::tbody,
nsGkAtoms::tfoot)) {
// If we're entering a tbody, we can persist the index guess we were passed,
// as the newly added items are in the same section as us, however, if we're
// entering thead or tfoot we will have to re-scan.
bool isTBody = aChild->IsHTMLElement(nsGkAtoms::tbody);
int32_t indexGuess = isTBody ? aIndexGuess : -1;
for (nsIContent* inner = aChild->GetFirstChild();
inner; inner = inner->GetNextSibling()) {
indexGuess = HandleInsert(aChild, inner, indexGuess);
}
return isTBody ? indexGuess : -1;
}
if (!aChild->IsHTMLElement(nsGkAtoms::tr)) {
return aIndexGuess; // Nothing inserted, guess hasn't changed.
}
// We should have only been passed an insertion from an interesting container,
// so we can get the container we're inserting to fairly easily.
nsIAtom* section = aContainer == mParent
? nsGkAtoms::tbody
: aContainer->NodeInfo()->NameAtom();
// Determine the default index we would to insert after if we don't find any
// previous row, and offset our section boundaries based on the section we're
// planning to insert into.
size_t index = 0;
if (section == nsGkAtoms::thead) {
mBodyStart++;
mFootStart++;
} else if (section == nsGkAtoms::tbody) {
index = mBodyStart;
mFootStart++;
} else if (section == nsGkAtoms::tfoot) {
index = mFootStart;
} else {
MOZ_ASSERT(false, "section should be one of thead, tbody, or tfoot");
}
// If we already have an index guess, we can skip scanning for the previous row.
if (aIndexGuess >= 0) {
index = aIndexGuess;
} else {
// Find the previous row in the section we're inserting into. If we find it,
// we can use it to override our insertion index. We don't need to modify
// mBodyStart or mFootStart anymore, as they have already been correctly
// updated based only on section.
nsIContent* insertAfter = PreviousRow(section, aChild);
if (insertAfter) {
// NOTE: We want to ensure that appending elements is quick, so we search
// from the end rather than from the beginning.
index = mRows.LastIndexOf(insertAfter) + 1;
MOZ_ASSERT(index != nsTArray<nsCOMPtr<nsIContent>>::NoIndex);
}
}
#ifdef DEBUG
// Assert that we're inserting into the correct section.
if (section == nsGkAtoms::thead) {
MOZ_ASSERT(index < mBodyStart);
} else if (section == nsGkAtoms::tbody) {
MOZ_ASSERT(index >= mBodyStart);
MOZ_ASSERT(index < mFootStart);
} else if (section == nsGkAtoms::tfoot) {
MOZ_ASSERT(index >= mFootStart);
MOZ_ASSERT(index <= mRows.Length());
}
MOZ_ASSERT(mBodyStart <= mFootStart);
MOZ_ASSERT(mFootStart <= mRows.Length() + 1);
#endif
mRows.InsertElementAt(index, aChild);
return index + 1;
}
// nsIMutationObserver
void
TableRowsCollection::ContentAppended(nsIDocument* aDocument,
nsIContent* aContainer,
nsIContent* aFirstNewContent,
int32_t aNewIndexInContainer)
{
if (!nsContentUtils::IsInSameAnonymousTree(mParent, aFirstNewContent) ||
!InterestingContainer(aContainer)) {
return;
}
// We usually can't guess where we need to start inserting, unless we're
// appending into mParent, in which case we can provide the guess that we
// should insert at the end of the body, which can help us avoid potentially
// expensive work in the common case.
int32_t indexGuess = mParent == aContainer ? mFootStart : -1;
// Insert each of the newly added content one at a time. The indexGuess should
// make insertions of a large number of elements cheaper.
for (nsIContent* content = aFirstNewContent;
content; content = content->GetNextSibling()) {
indexGuess = HandleInsert(aContainer, content, indexGuess);
}
}
void
TableRowsCollection::ContentInserted(nsIDocument* aDocument,
nsIContent* aContainer,
nsIContent* aChild,
int32_t aIndexInContainer)
{
if (!nsContentUtils::IsInSameAnonymousTree(mParent, aChild) ||
!InterestingContainer(aContainer)) {
return;
}
HandleInsert(aContainer, aChild);
}
void
TableRowsCollection::ContentRemoved(nsIDocument* aDocument,
nsIContent* aContainer,
nsIContent* aChild,
int32_t aIndexInContainer,
nsIContent* aPreviousSibling)
{
if (!nsContentUtils::IsInSameAnonymousTree(mParent, aChild) ||
!InterestingContainer(aContainer)) {
return;
}
// If the element being removed is a `tr`, we can just remove it from our
// list. It shouldn't change the order of anything.
if (aChild->IsHTMLElement(nsGkAtoms::tr)) {
size_t index = mRows.IndexOf(aChild);
if (index != nsTArray<nsCOMPtr<nsIContent>>::NoIndex) {
mRows.RemoveElementAt(index);
if (mBodyStart > index) {
mBodyStart--;
}
if (mFootStart > index) {
mFootStart--;
}
}
return;
}
// If the element being removed is a `thead`, `tbody`, or `tfoot`, we can
// remove any `tr`s in our list which have that element as its parent node. In
// any other situation, the removal won't affect us, so we can ignore it.
if (!aChild->IsAnyOfHTMLElements(nsGkAtoms::thead, nsGkAtoms::tbody, nsGkAtoms::tfoot)) {
return;
}
size_t beforeLength = mRows.Length();
mRows.RemoveElementsBy([&] (nsIContent* element) {
return element->GetParent() == aChild;
});
size_t removed = beforeLength - mRows.Length();
if (aChild->IsHTMLElement(nsGkAtoms::thead)) {
// NOTE: Need to move both tbody and tfoot, as we removed from head.
mBodyStart -= removed;
mFootStart -= removed;
} else if (aChild->IsHTMLElement(nsGkAtoms::tbody)) {
// NOTE: Need to move tfoot, as we removed from body.
mFootStart -= removed;
}
}
void
TableRowsCollection::NodeWillBeDestroyed(const nsINode* aNode)
{
// Set mInitialized to false so CleanUp doesn't try to remove our mutation
// observer, as we're going away. CleanUp() will reset mInitialized to true as
// it returns.
mInitialized = false;
CleanUp();
}
/* --------------------------- HTMLTableElement ---------------------------- */
HTMLTableElement::HTMLTableElement(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo)