Closed
Bug 1364816
Opened 8 years ago
Closed 8 years ago
Object.getOwnPropertyNames(window) is slow
Categories
(Core :: DOM: Core & HTML, enhancement)
Core
DOM: Core & HTML
Tracking
()
Tracking | Status | |
---|---|---|
firefox55 | --- | fixed |
People
(Reporter: mstange, Assigned: bzbarsky)
References
(Blocks 1 open bug)
Details
Attachments
(7 files, 1 obsolete file)
356 bytes,
text/html
|
Details | |
587 bytes,
text/html
|
Details | |
6.94 KB,
patch
|
qdot
:
review+
|
Details | Diff | Splinter Review |
4.62 KB,
patch
|
qdot
:
review+
|
Details | Diff | Splinter Review |
2.36 KB,
patch
|
jandem
:
review+
|
Details | Diff | Splinter Review |
10.99 KB,
patch
|
qdot
:
review+
jandem
:
review+
|
Details | Diff | Splinter Review |
9.71 KB,
patch
|
qdot
:
review+
|
Details | Diff | Splinter Review |
On my machine, this testcase prints around 16ms in Firefox and 0.2ms in Chrome.
Profile: https://perfht.ml/2qhHW9e
We seem to be creating all bindings that can be accessed on the window object, even though we only need the names of the window properties, not their values.
Reporter | ||
Comment 1•8 years ago
|
||
Reporter | ||
Comment 2•8 years ago
|
||
This bug is easy to hit with a common JavaScript toolchain: The "transform-runtime" plugin for the babel compiler transitively includes the following module, which calls Object.getOwnPropertyNames(window) during initialization:
https://github.com/zloirock/core-js/blob/master/library/modules/_object-gopn-ext.js
I've created a simple repository that shows the babel settings and an example JS file which are sufficient to encounter this bug: https://github.com/mstange/testcase-babel-windowbinding
This is also affecting perf.html.
Reporter | ||
Updated•8 years ago
|
Flags: needinfo?(ehsan)
Comment 3•8 years ago
|
||
That is only the first access to those properties, right?
Do I recall correctly that Blink/V8 pay the penalty earlier, when creating the Window object.
Reporter | ||
Comment 4•8 years ago
|
||
Yes, only on the first access.
I don't know when Blink pays this penalty, but it would be nice to find out how much of a penalty it is for them.
Comment 5•8 years ago
|
||
bz or peterv would know better.
IIRC at least at some point creating a new Window object was slower in Blink than in Gecko.
Assignee | ||
Comment 6•8 years ago
|
||
Our window creation is way faster than Blink's precisely because we do the work lazily.
That said, I'd be interested in seeing how the second call performs relative to the first.
> We seem to be creating all bindings that can be accessed on the window object, even though we only need the names of the window properties, not their values
Consider this testcase:
<script>
delete window.Node;
var arr = Object.getOwnPropertyNames(window);
</script>
This needs to NOT include "Node" in arr. The way we handle this is by actually doing the relevant lookups in getOwnPropertyNames, and the lookups are what instantiate the objects, because they have no way to know who's doing the lookup and why.
I suppose one thing we could do is only do lookups for the things that have a cached proto (and which have hence already been looked up on the window before). That might make the first call faster, but would probably make subsequent ones slower...
Assignee | ||
Comment 7•8 years ago
|
||
So the second call takes 0.2ms or so over here for me right now.
Assignee | ||
Comment 8•8 years ago
|
||
Assignee | ||
Comment 9•8 years ago
|
||
And the non-webidl bits make this even harder, of course. The basic problem is that right now we get the list of all the names that might be relevant, then use JS_HasUCProperty to test whether they've been deleted or not. But JS_HasUCProperty ends up instantiating lazy stuff, via the resolve hook...
Assignee | ||
Comment 10•8 years ago
|
||
Oh, and if you want a testcase that compares global creation between Blink and Gecko, see https://jsfiddle.net/76vqay5e/ which shows that we need about 2ms to create a global, while Chrome needs about 4 on my machine. They used to be _way_ slower here.
In either case, Safari needs about 1ms _and_ can do fast getOwnPropertyNames, so we should really try to figure out something here.
Assignee: nobody → bzbarsky
Flags: needinfo?(ehsan)
Comment 11•8 years ago
|
||
This also shows up on Speedometer v2's Angular2-TypeScript-TodoMVC-Adding100Items test. They do about 15 of these getOwnPropertyNames(window) calls but the first one is the slowest, of course (takes at least 10 ms here).
They're only doing an indexOf and don't actually care about the property value:
function h(t, e, n) {
for (var r = t; r && -1 === Object.getOwnPropertyNames(r).indexOf(e);)
r = Object.getPrototypeOf(r);
!r && t[e] && (r = t);
var o, i = x(e);
return r && !(o = r[i]) && (o = r[i] = r[e], r[e] = p(e, n(o, i, e))), o
}
bz, what if we had a separate class hook to get a list of "would resolve" properties? How hard is it to implement that on the Gecko side?
There's also the JSNewEnumerateOp op, maybe we could (re)use that somehow.
Blocks: Speedometer_V2
Whiteboard: [qf]
Comment 12•8 years ago
|
||
I'd be happy to look into this myself btw, next week or so. This could easily shave off 10-20 ms of a lot of page loads.
Flags: needinfo?(bzbarsky)
Comment 13•8 years ago
|
||
One problem with the "just get a list of properties" approach is that it could cause inconsistent property iteration order. We already have that problem, though, due to the resolve hook...
Comment 14•8 years ago
|
||
(In reply to Boris Zbarsky [:bz] (if a patch has no decent message, automatic r-) from comment #10)
> In either case, Safari needs about 1ms _and_ can do fast
> getOwnPropertyNames, so we should really try to figure out something here.
They have a getOwnPropertyNames hook. The window implementation is here (if I'm reading the code correctly):
https://github.com/WebKit/webkit/blob/a0da9778cb6d06865a146b9161ce6b83dc857161/Source/WebCore/bindings/js/JSDOMWindowCustom.cpp#L399
Updated•8 years ago
|
Whiteboard: [qf] → [qf:p1]
Assignee | ||
Comment 15•8 years ago
|
||
> what if we had a separate class hook to get a list of "would resolve" properties? How hard is it to implement that on the Gecko side?
That's not hard to implement, depending on what "would resolve" means, but how would it solve the problem we have here?
> We already have that problem, though, due to the resolve hook
Right.
> They have a getOwnPropertyNames hook. The window implementation is here (if I'm reading the code correctly):
Right, but this doesn't seem to be doing any lazy-resolution stuff. It's just handling the cross-origin bits, and if not delegating to Base::getOwnPropertyNames.
Flags: needinfo?(bzbarsky)
Comment 16•8 years ago
|
||
(In reply to Boris Zbarsky [:bz] (if a patch has no decent message, automatic r-) from comment #15)
> > what if we had a separate class hook to get a list of "would resolve" properties? How hard is it to implement that on the Gecko side?
>
> That's not hard to implement, depending on what "would resolve" means, but
> how would it solve the problem we have here?
The problem we have here is that getOwnPropertyNames(window) et al will currently resolve all properties on the object and that's very slow because we have a ton of (DOM) prototype objects to initialize, right? So the proposal is just to add a separate hook that returns just the list of properties and using that instead of resolving everything eagerly.
Assignee | ||
Comment 17•8 years ago
|
||
That hook is what window actually implements already. Then the bindings go and look up all those props so we can avoid returning the ones that have been deleted... That's where the resolving comes from. So we need some way to deal with the "property got deleted" issue. Per IRC discussion just now, we will use newenumerate and have it only return things that have NOT been resolved yet. Need a bit more machinery on the DOM side to make this work, but should be doable.
Comment 18•8 years ago
|
||
As I mentioned on IRC, we will need some way to make Object.freeze and friends work with that setup. Right now js::PreventExtensions calls js::GetPropertyKeys to force resolving all lazy properties. We could fix that by calling new-enumerate and then resolving each property it returns.
(Hopefully at some point we will be able to kill the old-enumerate hook and simplify all this...)
Assignee | ||
Comment 19•8 years ago
|
||
> we will need some way to make Object.freeze and friends work with that setup. Right now js::PreventExtensions
Shouldn't be an issue. I don't think it's possible to invoke js::PreventExtensions on a Window directly, and when invoked on a WindowProxy it won't reach the GetPropertyKeys call at all. Instead, it will do js::Proxy::preventExtensions, which will land in nsOuterWindowProxy::preventExtensions which fails the operation as required by https://html.spec.whatwg.org/multipage/browsers.html#windowproxy-preventextensions
Assignee | ||
Comment 20•8 years ago
|
||
Attachment #8874726 -
Flags: review?(kyle)
Assignee | ||
Comment 21•8 years ago
|
||
Attachment #8874727 -
Flags: review?(kyle)
Assignee | ||
Comment 22•8 years ago
|
||
Attachment #8874728 -
Flags: review?(kyle)
Attachment #8874728 -
Flags: review?(jdemooij)
Assignee | ||
Comment 23•8 years ago
|
||
Attachment #8874729 -
Flags: review?(jdemooij)
Assignee | ||
Comment 24•8 years ago
|
||
Attachment #8874730 -
Flags: review?(kyle)
Attachment #8874730 -
Flags: review?(jdemooij)
Comment 25•8 years ago
|
||
Comment on attachment 8874728 [details] [diff] [review]
part 3. Switch NeedResolve bindings to using a newResolve hook instead of a resolve hook
Review of attachment 8874728 [details] [diff] [review]:
-----------------------------------------------------------------
::: dom/bindings/Codegen.py
@@ +8951,5 @@
> prefix = dedent("""
> + // This is OK even though we're a newEnumerate hook: this will
> + // define the relevant properties on the global, and the JS
> + // engine will pick those up, because it looks at the object's
> + // properties after this hook has returned.
Mind filing a follow-up bug to optimize JS_EnumerateStandardClasses? I can take it.
Attachment #8874728 -
Flags: review?(jdemooij) → review+
Updated•8 years ago
|
Attachment #8874729 -
Flags: review?(jdemooij) → review+
Comment 26•8 years ago
|
||
Comment on attachment 8874730 [details] [diff] [review]
part 5. Make getting window names a bit faster by avoiding various intermediate strings
Review of attachment 8874730 [details] [diff] [review]:
-----------------------------------------------------------------
Thanks for doing this!
::: dom/base/nsGlobalWindow.cpp
@@ +5170,5 @@
> // just goes ahead and recreates them.
> + JSString* str = JS_AtomizeUCStringN(aCx,
> + entry->mKey.BeginReading(),
> + entry->mKey.Length());
> + if (!str || !aNames.append(NON_INTEGER_ATOM_TO_JSID(str))) {
Did you see malloc/realloc in your profiles? Wondering if it makes sense to do aNames.reserve(x) before the loop where x is a reasonable estimate of the number of properties we expect.
::: dom/bindings/WebIDLGlobalNameHash.cpp
@@ +321,5 @@
> !cache->EntrySlotIfExists(entry->mConstructorId)) &&
> (!entry->mEnabled || entry->mEnabled(aCx, aObj))) {
> + JSString* str = JS_AtomizeStringN(aCx, sNames + entry->mNameOffset,
> + entry->mNameLength);
> + if (!str || !aNames.append(NON_INTEGER_ATOM_TO_JSID(str))) {
Same here.
Attachment #8874730 -
Flags: review?(jdemooij) → review+
Assignee | ||
Comment 27•8 years ago
|
||
> Mind filing a follow-up bug to optimize JS_EnumerateStandardClasses? I can take it.
Yep, bug 1370608.
> Did you see malloc/realloc in your profiles?
A bit. Especially before the "part 5" patch. I just did a profile with this entire patch stack, and the summary is like so. Of the time spent under the Object.getOwnPropertyNames call, about 53% is under the window enumerate hook (the rest is Snapshot, proxy machinery, etc). Almost all that time is under WebIDLGlobalNameHash::GetNames, and breaks down as follows (all percentages of the getOwnPropertyNames call):
* 25% js::Atomize
* 5% hashtable iterator
* 3% Vector::growStorageBy (1% malloc(), some free(), etc).
* 0.5% BarrierMethods::exposeToJS (filed bug 1370614 on this).
and most of the last is a _very_ long tail of tests for whether the given constructor is actually supposed to be exposed on the given window.
So it doesn't look like the malloc/realloc is a huge issue at first glance. Probably because our list has 900+ entries, so we only realloc 10 times or so as we go....
Blocks: 1370614
Updated•8 years ago
|
Attachment #8874726 -
Flags: review?(kyle) → review+
Updated•8 years ago
|
Attachment #8874727 -
Flags: review?(kyle) → review+
Updated•8 years ago
|
Attachment #8874728 -
Flags: review?(kyle) → review+
Assignee | ||
Comment 28•8 years ago
|
||
Assignee | ||
Updated•8 years ago
|
Attachment #8874728 -
Attachment is obsolete: true
Comment 29•8 years ago
|
||
Comment on attachment 8874943 [details] [diff] [review]
Part 3 updated to not expose DOMConstructor, because it's special like that
Review of attachment 8874943 [details] [diff] [review]:
-----------------------------------------------------------------
::: dom/base/nsGlobalWindow.cpp
@@ +5149,5 @@
> + // return all possible WebIDL names, because we don't really support
> + // deleting these names off our Xray; trying to resolve them will just make
> + // them come back. In the former case, we want to avoid returning deleted
> + // names. But the JS engine already knows about the non-deleted
> + // already-resolve names, so we can just return the so-far-unresolved ones.
wording: already-resolved?
Attachment #8874943 -
Flags: review+
Updated•8 years ago
|
Attachment #8874730 -
Flags: review?(kyle) → review+
Comment 30•8 years ago
|
||
Pushed by bzbarsky@mozilla.com:
https://hg.mozilla.org/integration/mozilla-inbound/rev/68989f96eb93
part 1. Store the relevant constructor id in the entries in the WebIDLGlobalNameHash. r=qdot
https://hg.mozilla.org/integration/mozilla-inbound/rev/4b72b120485d
part 2. Add a way to ask the WebIDLGlobalNameHash for only the names that have not yet been resolved on the given window. r=qdot
https://hg.mozilla.org/integration/mozilla-inbound/rev/ab1e672183ad
part 3. Switch NeedResolve bindings to using a newResolve hook instead of a resolve hook. r=qdot,jandem
https://hg.mozilla.org/integration/mozilla-inbound/rev/79157ef8e455
part 4. Add a JS friend API for getting an jsid from a known-atom JSString*. r=jandem
https://hg.mozilla.org/integration/mozilla-inbound/rev/2d37f2bce087
part 5. Make getting window names a bit faster by avoiding various intermediate strings. r=qdot,jandem
Comment 31•8 years ago
|
||
bugherder |
https://hg.mozilla.org/mozilla-central/rev/68989f96eb93
https://hg.mozilla.org/mozilla-central/rev/4b72b120485d
https://hg.mozilla.org/mozilla-central/rev/ab1e672183ad
https://hg.mozilla.org/mozilla-central/rev/79157ef8e455
https://hg.mozilla.org/mozilla-central/rev/2d37f2bce087
Status: NEW → RESOLVED
Closed: 8 years ago
status-firefox55:
--- → fixed
Resolution: --- → FIXED
Target Milestone: --- → mozilla55
Updated•6 years ago
|
Component: DOM → DOM: Core & HTML
Updated•3 years ago
|
Performance Impact: --- → P1
Whiteboard: [qf:p1]
You need to log in
before you can comment on or make changes to this bug.
Description
•