Closed Bug 863966 Opened 11 years ago Closed 11 years ago

Consider creating some sort of parsed-selector cache for querySelector(All)

Categories

(Core :: DOM: Core & HTML, defect)

x86
macOS
defect
Not set
normal

Tracking

()

RESOLVED FIXED
mozilla27

People

(Reporter: bzbarsky, Assigned: almasry.mina)

References

Details

(Whiteboard: [mentor=bz][lang=c++])

Attachments

(3 files, 16 obsolete files)

149 bytes, text/html
Details
430 bytes, text/html
Details
9.11 KB, patch
bzbarsky
: review+
Details | Diff | Splinter Review
Need to figure out what, if anything, needs to go into the cache key other than the obvious selector string.  Performance testcase:

<script>
  var el = document.createElement("form");
  var count = 10000000;
  var start = new Date;
  for (var i = 0; i < count; ++i)
    el.querySelector("a");
  var end = new Date;
  document.write((end - start) / count * 1e6);
</script>
Mina, if you get bored.... ;)
Always :-D
QA Contact: almasry.mina
Assignee: nobody → almasry.mina
QA Contact: almasry.mina
Attached file make-errors (obsolete) —
I'm working on this, but I've hit something of a wall. I believe what you want is to add a hashtable for each node, that hashes the keys to an nsSimpleContentList. Something like:

ns(RefPtr|Class)Hashtable <nsStringHashKey, nsSimpleContentList>

My problem seems to be that nsSimpleContentList can't work with any hashtable. Any time I define a hashtable like that I get strange make errors like in the attachment. What to do?
You can't include nsIContent.h in nsINode.h; that'd cause an include cycle. It looks like you probably can include nsContentList.h, but that's probably not a good idea given all the work we're doing to reduce includes. See if you can get away with just including nsRefPtrHashtable.h and forward-declaring everything else?

+  struct CacheEntry
+  {
+    nsSimpleContentList mList;
+    bool mAreAll;
+  };

This won't work; mList should be a pointer. (Though you don't seem to actually use it?)
Attached patch Patch (obsolete) — Splinter Review
Attachment #791154 - Attachment is obsolete: true
Attached file test-crash (obsolete) —
Attached patch WIP patch (obsolete) — Splinter Review
Attachment #791867 - Attachment is obsolete: true
So I got passed that and here is a WIP patch. The patch builds, but the tests fail with a memory crash, which makes me think I have a memory leak somewhere or this method of caching takes too much memory. I'm still looking for the cause, but I thought I'd post the patch here anyway to check that this is what you want, and if it's obvious where the leak is please let me know. I think I'm using the nsClassHashtable correctly..
Attachment #791872 - Flags: review?(mounir)
Attachment #791872 - Flags: review?(mounir) → review?(bzbarsky)
Comment on attachment 791872 [details] [diff] [review]
WIP patch

So first off, adding a hashtable to every single node isn't desirable: it uses too much memory.

Why can't we use a static, or per-document, hashtable here?  Furthermore, I suspect we really want something using nsExpirationTracker so we don't leak the memory for the lifetime of the document, or worse yet the process.

Your memory errors come from explicitly deleting the refcounted list in ~CacheEntry.  You'd probably just want to make mList an nsRefPtr instead.

But more to the point, we don't want to cache the _list_.  We want to produce a new list each time, not least because its contents will change as the DOM is modified.  What we want to cache is the nsCSSSelectorList*.  The new setup would probably have the cache own it and just return an nsCSSSelectorList* where we currently have an nsAutoPtr<nsCSSSelectorList>...
Attachment #791872 - Flags: review?(bzbarsky) → review-
Attached patch Patch v1 (obsolete) — Splinter Review
My bad I was way off. Better?
Attachment #791869 - Attachment is obsolete: true
Attachment #791872 - Attachment is obsolete: true
Attachment #795091 - Flags: review?(bzbarsky)
Comment on attachment 795091 [details] [diff] [review]
Patch v1

Better, but clearly untested: for example, nothing in this patch ever sets mSelectorCache, so trying to use querySelector with this patch crashes.

I think it's probably simpler to just make mSelectorCache be a SelectorCache object, not a pointer to a SelectorCache object.

>+      nsClassHashtable<nsStringHashKey, nsCSSSelectorList> mTable;

I don't like making this public.  Better for it to be private and 
have a method to look up a given string on a SelectorCache.

It's worth explicitly documenting that we're not calling MarkUsed because it would just slow down lookups and because we're OK expiring things after a few second even if they're being used.

>+  nsAutoPtr<SelectorCache> mSelectorCache;

Likewise, this should not be public.  Instead, have a method for getting a SelectorCache& or something?

CacheSelectorList should be documented as taking ownership of aSelectorList.

>+    if (NS_FAILED(rv)) {

Then I think you need to delete selectorList.

I'd like to see a patch with the above fixed, and some numbers on this bug's performance testcase...
Attachment #795091 - Flags: review?(bzbarsky) → review-
Mina, are you going to have time to wrap this up?
Flags: needinfo?(almasry.mina)
Yes. Sorry for the delay but I'm back on it.

If I'm holding anyone back with the delay feel free to assign it to someone else. I'll understand :)
Flags: needinfo?(almasry.mina)
No one is blocked on this right now.
Blocks: 917258
Well, I was slightly wrong.  Some benchmarks are blocked on it.  jandem, what's our timeframe for peacekeeper?
(In reply to Boris Zbarsky [:bz] from comment #15)
> Well, I was slightly wrong.  Some benchmarks are blocked on it.  jandem,
> what's our timeframe for peacekeeper?

The browser grand prix and other websites use peacekeeper to measure "JS speed" and winning the grand prix is one of our goals. That's why I'm looking into peacekeeper and browsermark this week. So the timeframe is "ASAP"...

If we could finish and land this soonish I'd appreciate it :)
Attached patch Patch v2 (obsolete) — Splinter Review
Sorry for the delay. Here is an updated patch.
Attachment #795091 - Attachment is obsolete: true
Attachment #806568 - Flags: review?(bzbarsky)
Attached patch Patch v3 (obsolete) — Splinter Review
Added the MarkUsed doc string.
Attachment #806568 - Attachment is obsolete: true
Attachment #806568 - Flags: review?(bzbarsky)
Attachment #806569 - Flags: review?(bzbarsky)
Attached file no-caching.html (obsolete) —
Performance of querySelector without caching. Time in microseconds. Note that count is 1,000,000 and not 10,000,000. I hope that's okay.
Attached file querySelector-4gen-1000.html (obsolete) —
Performance with caching. 14,320 ns was improved to 3,175 ns.
Attached patch Interdiff (obsolete) — Splinter Review
This is the interdiff, too.

My bad the time in the tests is in nanoseconds, not milliseconds.
Attached file no-caching.html
Attachment #806570 - Attachment is obsolete: true
Attachment #806572 - Attachment is obsolete: true
Comment on attachment 806569 [details] [diff] [review]
Patch v3

>+++ b/content/base/public/nsIDocument.h
>+private:
>+  class SelectorCacheKey

So I think the private class declarations should live over with the other private functions on nsIDocument, the member should live next to other members, and the public function (and a forward-declaration of class SelectorCache) should be next to other public functions.

>+      SelectorCacheKey(const nsAString& aString) : mKey(aString) { }

This should have MOZ_COUNT_CTOR/MOZ_COUNT_DTOR bits.

>+      void CacheList(const nsAString& aSelector, nsCSSSelectorList* aSelectorList);

This needs documentation about the fact that it takes ownership of aSelectorList.

>+      // We do not call MarkUsed because it would just slow down lookups and

This comment should be up in GetList.

>+++ b/content/base/src/nsINode.cpp
>   NS_ENSURE_TRUE(selectorList, NS_OK);

This needs to be before the CacheList call.

r=me with those issues fixed.  Thank you!
Attachment #806569 - Flags: review?(bzbarsky) → review+
Attached patch Patch v4 (obsolete) — Splinter Review
Try: https://tbpl.mozilla.org/?tree=Try&rev=3adda0209d44

I need another review I think. I had to move SelectorCache's constructor from nsIDocument.h to nsDocument.cpp. The reason is nsCSSSelectorList is forward declared, and therefore the template instantiation should be in the .cpp file.
Attachment #806569 - Attachment is obsolete: true
Attachment #808898 - Flags: review?(bzbarsky)
> The reason is nsCSSSelectorList is forward declared, and therefore the template
> instantiation should be in the .cpp file.

I don't follow this.  Is the issue that the mTable member's constructor wants to see the declaration of nsCSSSelectorList or something?  What failed with the old approach?
Flags: needinfo?(malmasry)
Attached file make-errors (obsolete) —
Here is the make error I get.

The instantiation of mTable contains a call to delete nsCSSSelectorList, and so nsCSSSelectorList needs to be a complete type by then. The instantiation happens at the constructor of SelectorCache, so the constructor can't be in nsIDocument.h (nsCSSSelectorList is only forward declared there) but can be in nsDocument.cpp.

This is my fault big time. It looks like the patch that was granted a review was the wrong patch, and it didn't build. But the difference between that patch and the correct one is only moving the constructor.
Flags: needinfo?(malmasry)
Ah, I see.  It's the instantiation of the clearEntry hook that's relevant.
Comment on attachment 808898 [details] [diff] [review]
Patch v4

r=me
Attachment #808898 - Flags: review?(bzbarsky) → review+
Keywords: checkin-needed
I wonder whether ~nsExpirationTracker expires everything that's left in it.  If it doesn't, we could be leaking SelectorCacheKey.  We'd know for sure if my review comments on that (MOZ_CONT_CTOR/DTOR) had gotten addressed and if I hadn't missed pointing the problem out again the second time around.  :(

One other thing: reading the expiration tracker code more closely, it looks like it could expire a cached entry while we're selector-matching.  We should probably make our NotifyExpired do the delete async...
What we should consider doing is AgeAllGenerations() in ~SelectorCache and seeing if that helps.  

And a _debug_ try run.  Way more useful than an opt one, typically.
Mina, ping?
Flags: needinfo?(malmasry)
Blocks: 922063
Blocks: 922071
Attached patch Patch v5 (obsolete) — Splinter Review
Sorry for the delay, again. Here is a test passing patch with the selector cache key deleted async.

Try push: https://tbpl.mozilla.org/?tree=Try&rev=910f3f415d75
Attachment #806574 - Attachment is obsolete: true
Attachment #808898 - Attachment is obsolete: true
Attachment #808941 - Attachment is obsolete: true
Attachment #812486 - Flags: review?(bzbarsky)
Flags: needinfo?(malmasry)
Comment on attachment 812486 [details] [diff] [review]
Patch v5

>+#include "nsThreadUtils.h"

Let's not do that.

Instead, please move the (virtual, so it's not like it's getting inline anyway, right?) NotifyExpired to the .cpp file.  And mark it as MOZ_OVERRIDE while we're here.

>+        nsIDocument::SelectorCacheKeyDeleter asyncDelete(aSelector);

No, this is wrong.  You want to actually post the event.  Something like this:

  NS_DispatchToCurrentThread(new SelectorCacheKeyDeleter(aSelector));

r=me with those changes.
Attachment #812486 - Flags: review?(bzbarsky) → review+
I made these changes, but now the tests fail. The fails are mainly leaks of SelectorCacheKey.

https://tbpl.mozilla.org/?tree=Try&rev=238684cf2358

What to do here? I think with the mistake I made in the patch in comment #35, the SelectorCacheKey gets deleted synchronously and that passes the tests. Can we maybe leave it that way and call MarkUsed() instead to keep nsExpirationTracker from expiring while we are doing matching?
Attachment #812486 - Attachment is obsolete: true
Attachment #813073 - Flags: feedback?(bzbarsky)
You can't use MarkUsed to prevent expiration due to memory-pressure notifications, which were the thing that could cause dangling pointers.

What's confusing me is that we don't show any leaks of SelectorCacheKeyDeleter, so presumably the event in fact got posted.  I wonder whether we just manage to take our leak measurement while the event is still pending or something...

Can you reproduce the leak locally?
Attached patch patch v6 (obsolete) — Splinter Review
Okay SelectorCacheKeyDeleter was leaking, I just didn't have the CTOR/DTOR bits there.

The problem was the I have to save the nsRunnable instance into an nsCOMPtr before passing it into NS_DispatchToCurrentThread. It now passes the tests again: https://tbpl.mozilla.org/?tree=Try&rev=e3e6e4c2cd34

While you're at it could you please explain why does this work? Why does doing the delete in NotifyExpired async make it that AgeAllGenerations() called with a memory-pressure won't expire things while we are selector matching?

Why did I need to save new SelectorCacheKeyDeleter into an nsCOMPtr? I thought the new SelectorCacheKeyDeleter would start with refcount 0, then get incremented to 1 when it is passed to NS_DispatchToCurrentThread, then get decremented back to 0 and released after it runs which is what we want.

Thanks!
Attachment #813073 - Attachment is obsolete: true
Attachment #813073 - Flags: feedback?(bzbarsky)
Attachment #813202 - Flags: review?(bzbarsky)
> Okay SelectorCacheKeyDeleter was leaking,

Huh.  I wonder why that leak wasn't getting reported...  Oh, because there was no initial addref!

So what happened there is that the thread had already shut down, so  NS_DispatchToCurrentThread didn't do anything useful with the argument.  Fun.

> Why does doing the delete in NotifyExpired async make it that AgeAllGenerations()
> called with a memory-pressure won't expire things while we are selector matching?

So the worry I had is that we would look up a selector in the cache, start selector matching, allocate some memory during selector matching, hit a low-memory condition, get a memory-pressure notification, the expiration tracker would expire all the stuff in it, delete the selector.  Then we unwind the stack back to selector matching, try to work with the now-deleted selector object and crash.

If we do the delete async, that ensures that we unwind out of selector matching before the selector gets deleted.

> then get incremented to 1 when it is passed to NS_DispatchToCurrentThread,

Nope.  nsThread::Dispatch checks for thread shutdown before taking a reference to the argument.

So with the stack ref, the delete will happen async unless we're in shutdown, in which case it'll happen sync when the nsCOMPtr goes off the stack.  Which is fine, because if we're at that point in shutdown we're not in the middle of a querySelector call, I would hope.  ;)
Comment on attachment 813202 [details] [diff] [review]
patch v6

NotifyExpired should still be marked virtual and MOZ_OVERRIDE.

r=me with that.
Attachment #813202 - Flags: review?(bzbarsky) → review+
Attached patch patch v7 (obsolete) — Splinter Review
Extra thanks for your patience on this one Boris.
Attachment #813202 - Attachment is obsolete: true
Try push in comment #39. Changes in patch from that version to this one are trivial and I made sure it compiles locally. If you need another push, just let me know. Thanks!
Keywords: checkin-needed
Backed out in http://hg.mozilla.org/integration/mozilla-inbound/rev/97bdee4bd259. I still have retriggers running on the pushes before to be sure, but I expect this'll wind up being the cause of the frequent Android robocop-2 crashes like https://tbpl.mozilla.org/php/getParsedLog.php?id=28757488&tree=Mozilla-Inbound. Unfortunately, your -b d Try run didn't run any Android (or b2g) tests, since we only run them on opt builds. If anyone ever hassles you about the "waste" of running -b do -p all -u all try runs, refer them to me, I'll yell until they wish they'd never brought it up.
Hmm.  So that's a memory-pressure notification coming through, which calls sExpirationTracker<nsIDocument::SelectorCacheKey, 4u>::ExpirationTrackerObserver::Observe and that lands us in AgeAllGenerations.

The actual crash is in the nsTarray_base::Length() call that AgeOneGeneration does...

So how did we manage to end up with a bogus tarray there?  Might be worth adding logging to ~nsExpirationTracker and ExpirationTrackerObserver::Destroy and ExpirationTrackerObserver::Observe to log the mOwner and this and mObserver object pointers, just to see whether we're getting notified on a destroyed object somehow.
I will look at this more soon and to see if I have a proposal on what to do, but so far I have:

this try push: https://tbpl.mozilla.org/?tree=Try&rev=8d60741bf8b6

With this log (search for MINA): https://tbpl.mozilla.org/php/getParsedLog.php?id=28812924&tree=Try&full=1

Just before the crash we call AgeAllGenerations on 0x59569838 from the observer 0x5be2acb0, but a little above that we enter the destructor of 0x59569838, and we call Destroy on 0x5be2acb0.

My guess is that queued notifications still get processed after Destroy removes the event listener, or that the event listener is removed while a memory pressure notification is being processed so there is a race there.
It sure looks to me like the observer service grabs all the observers that exist when NotifyObservers is called (http://mxr.mozilla.org/mozilla-central/source/xpcom/ds/nsObserverService.cpp#319 and http://mxr.mozilla.org/mozilla-central/source/xpcom/ds/nsObserverList.cpp#88), so if the expiration tracker were destroyed during a memory-pressure notification, the observer service would not notice the removal of the tracker's memory-pressure observer. I'm surprised this hasn't ever come up before.
Yeah, we should have Destroy null out mOwner and have Observe null-check it.
Attached patch patch v8Splinter Review
Great, that worked. Try: https://tbpl.mozilla.org/?tree=Try&rev=aace95d87de0
Attachment #813321 - Attachment is obsolete: true
Attachment #814675 - Flags: review?(bzbarsky)
Comment on attachment 814675 [details] [diff] [review]
patch v8

r=me
Attachment #814675 - Flags: review?(bzbarsky) → review+
Okay that try was -b do -p all -u all so I think I'm good here. Marking checkin needed. Fingers crossed :(
Keywords: checkin-needed
Yay:

  (Improvement) Mozilla-Inbound-Non-PGO - Dromaeo (CSS) - Ubuntu HW 12.04 x64 - 3.85%
Yay!
https://hg.mozilla.org/mozilla-central/rev/965b4f247f4b
Status: NEW → RESOLVED
Closed: 11 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla27
Component: DOM → DOM: Core & HTML
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: