8s of hang when switching folders on Gdrive (~3x slower than Chrome)
Categories
(Core :: JavaScript Engine, task, P3)
Tracking
()
People
(Reporter: mayankleoboy1, Assigned: mayankleoboy1)
References
(Blocks 2 open bugs)
Details
Attachments
(9 files, 2 obsolete files)
|
Bug 2031153 - Part 1: Add helpers for walking an object's sparse elements. r?#spidermonkey-reviewers
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
3.35 KB,
text/html
|
Details | |
|
3.64 KB,
text/html
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review |
I had a folder on GDrive with lots of images. I then moved one directory up and noticed a hang.
I then repeated the steps and captured a profile.
https://share.firefox.dev/4tLtBPa
Maybe something obvious to improve?
Comment 1•5 months ago
|
||
It looks to me like we are spending most of our time in Array.join because we called toString on an array. The most interesting part of the profile is the time we spend in GetProperty under GetArrayElement, which implies that we aren't looking at a dense array. I suspect that part of this is just the website doing something inefficient.
It might be interesting to see an equivalent profile in Chrome. They have slightly better support for sparse arrays. I don't think we have any easy wins in that area, though.
| Assignee | ||
Comment 2•5 months ago
|
||
Here is another profile where i cut 350 photos from a folder and paste them into another
https://share.firefox.dev/4dLYDBA
| Assignee | ||
Comment 3•5 months ago
|
||
(In reply to Iain Ireland [:iain] from comment #1)
It looks to me like we are spending most of our time in Array.join because we called toString on an array. The most interesting part of the profile is the time we spend in GetProperty under GetArrayElement, which implies that we aren't looking at a dense array. I suspect that part of this is just the website doing something inefficient.
It might be interesting to see an equivalent profile in Chrome. They have slightly better support for sparse arrays. I don't think we have any easy wins in that area, though.
Firefox: https://share.firefox.dev/4tFLlv1 (8.5s)
Chrome: https://share.firefox.dev/3QlplqV (3.s)
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Updated•5 months ago
|
Comment 4•5 months ago
|
||
Yeah, looks like they're just faster on Array.join on sparse arrays (which makes sense, because they have a nicer data representation: see bug 1339265).
Here's a microbenchmark that reproduces the time we spend on GetArrayElement:
let arr = [];
for (var i = 0; i < 1000; i++) {
arr[i * 4000] = 1;
}
function foo(arr) {
return arr.join("");
}
let start = performance.now()
for (var i = 0; i < 5; i++) {
foo(arr);
}
print(performance.now() - start);
There's also time in the profile spent in ValueToStringBuilder, which I can't immediately explain. V8 is probably also a bit faster there, but it's hard to tell because most of that logic is inlined into ArrayJoin.
Updated•5 months ago
|
| Assignee | ||
Comment 5•2 months ago
|
||
Adds ForEachOwnIndexedProperty and CollectOwnIndexedKeysInRange, replacing the
hand-rolled shape scans in ArraySetLength, maybeDensifySparseElements and for-in
enumeration.
Array.prototype.join already skipped the trailing holes of an array with no
indexed properties (bug 1964988), but went back to a generic lookup for every
index as soon as the array had any. It now walks only the indexes that exist.
Bug 1939342's delete fast path is generalized to "remove every element at index
= N" and reused by sort and by splice's tail deletion, which ran the same
per-index loop.
| Assignee | ||
Comment 6•2 months ago
|
||
ForEachOwnIndexedProperty and CollectOwnIndexedKeysInRange replace the
hand-rolled shape scans in ArraySetLength, maybeDensifySparseElements and for-in
enumeration, and SortIndexedKeys absorbs the sort those keys need.
The delete fast path is renamed to TryFastDeleteElementsAbove, since it removes
every element above an index and leaves .length to its caller.
No behavior change.
| Assignee | ||
Comment 7•2 months ago
|
||
Removing every element above an index is what Array.prototype.sort does when it
re-creates the holes that sorted to the end, and what splice does when it
deletes its tail. Both ran a per-index DeletePropertyOrThrow loop over the whole
range, so an array with sparse elements paid for every index in between.
| Assignee | ||
Comment 8•2 months ago
|
||
join already skipped the trailing holes of an array with no indexed properties
(bug 1964988), but went back to a generic lookup for every index as soon as the
array had any. It now walks only the indexes that exist.
| Assignee | ||
Comment 9•2 months ago
|
||
array_join walked every index of a hole run to append one separator each, so
joining an array that is mostly holes did work proportional to its length even
when the separator was empty and the run produced nothing at all.
The separator becomes a small type that knows how to append itself n times, so
the run is emitted in one call: nothing for an empty separator, one appendN for
a single Latin-1 character. Two-byte and multi-character separators still repeat
one at a time, since StringBuilder::appendN only takes a Latin1Char.
Updated•2 months ago
|
| Assignee | ||
Comment 10•2 months ago
|
||
the try run looks green: https://treeherder.mozilla.org/jobs?repo=try&revision=599b2373f4870777a2c34fc8d758509b8c42d269
Updated•2 months ago
|
Updated•2 months ago
|
Updated•2 months ago
|
| Assignee | ||
Comment 11•2 months ago
|
||
| Assignee | ||
Updated•2 months ago
|
Updated•2 months ago
|
Updated•2 months ago
|
| Assignee | ||
Comment 12•2 months ago
|
||
These patches are created by Claude. I cannot defend the patch line-by-line. Therefore, per Moz AI policy, I cannot be the author.
So the reviewer would need to find an author for these patches (and a co-author by me).
| Assignee | ||
Comment 13•2 months ago
|
||
I did not test with Gdocs, but here are the results with a modified version of the microbenchamark that Iain posted in comment 4:
Nightly: https://share.firefox.dev/4cj2QuQ (11s)
Patched with 1000x loopcount: https://share.firefox.dev/4hPyZ0J (190ms)
| Assignee | ||
Comment 14•2 months ago
|
||
| Assignee | ||
Comment 15•2 months ago
|
||
And these are the profiles on gdrive on image heavy folders (but not necessarily the one from comment 0) : https://share.firefox.dev/4fT6KvK, https://share.firefox.dev/4z5hcZR, https://share.firefox.dev/4wa9kn7
| Assignee | ||
Comment 16•2 months ago
|
||
What else shows up in the GDrive profiles?
Comment 17•2 months ago
|
||
We discussed this a bit on Matrix. We should check the patches help on Google Drive because we still spend a lot of time under ArrayJoin looking up properties, so it's possible the fast path doesn't apply there for some reason.
| Assignee | ||
Comment 18•2 months ago
|
||
(In reply to Jan de Mooij [:jandem] from comment #17)
We discussed this a bit on Matrix. We should check the patches help on Google Drive because we still spend a lot of time under
ArrayJoinlooking up properties, so it's possible the fast path doesn't apply there for some reason.
In the Google Drive workload, the arrays being joined have holes in their dense elements. Consequently, the denseElementsArePacked() check fails, HasOnlyDenseAndSparseElements() returns false, and SpiderMonkey falls completely back to the unoptimized ArrayJoinKernel slow path
| Assignee | ||
Comment 19•2 months ago
|
||
Nightly: All the three methods are slow
With current patches: Methods 1 and 3 are fast. Method 2 is slow. This is the reduced testcase for the GDrive profile.
| Comment hidden (obsolete) |
| Assignee | ||
Comment 21•1 month ago
|
||
removing the needinfo. As usual, Gemini gives bad designs.
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
| Assignee | ||
Comment 22•1 month ago
|
||
Adding a sparse element below the dense initialized length turns the dense
element there into a hole, so packed dense elements imply the dense and sparse
ranges are disjoint. The reverse does not hold: deleting a dense element marks
the elements non-packed without adding anything to the shape, and join then
gave up on an array whose ranges never overlapped.
Check for the overlap itself instead of requiring packed elements.
| Assignee | ||
Comment 23•1 month ago
|
||
The dense walk stops at the first element it can't stringify without running
script, and everything after it went through the generic loop, which looks up
every index. That is much worse than it sounds for an array with holes:
GetArrayElement's dense fast path falls through on a hole, so each one costs a
full prototype chain lookup that the dense walk did for free.
Run the walk again once the generic loop has handled the element that stopped
it, re-checking the fast path guard first since stringifying can run script.
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
| Assignee | ||
Comment 24•1 month ago
|
||
array_join walked every index of a hole run to append one separator each, so
joining an array that is mostly holes did work proportional to its length even
when the separator was empty and the run produced nothing at all.
The separator becomes a small type that knows how to append itself n times, so
the run is emitted in one call: nothing for an empty separator, one appendN for
a single Latin-1 character. Two-byte and multi-character separators still repeat
one at a time, since StringBuilder::appendN only takes a Latin1Char, and they
keep checking for interrupts as they go; a run that no longer has anywhere to
check is checked once before it starts.
The run stopped at lengths below UINT32_MAX, so the one length an array can't
exceed missed it by one. Nothing needs the stricter bound: the run counts in a
uint32_t and an index never reaches UINT32_MAX. Checking the premise of the
skipped range in debug builds walked all of it, which is the work the run exists
to avoid, so check each end of it instead.
| Assignee | ||
Comment 25•1 month ago
|
||
Sorting without a comparator reserved room for two Values per index and then
asked for every index, so an array holding a handful of elements at a large
length needed gigabytes and minutes rather than work proportional to what it
holds. Everything after the collection was already proportional to the element
count, and Part 2 made the trailing deletion so too.
Walk the sparse elements instead when the array has no other way to hold an
index, and size the sort buffer from what the walk finds. The dense and sparse
ranges are disjoint there, so appending one after the other keeps the elements
in index order, which sort has to be stable over.
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
Updated•1 month ago
|
| Assignee | ||
Comment 26•1 month ago
•
|
||
- SP3/JS3 perfcompare: https://perf.compare/compare-results?baseRev=1d1ff7e80efee61337eb23c8d64972e2c6a0cee2&baseRepo=try&newRev=a8c516639c9ce965bfa26427f3cc3c13a259dda9&newRepo=try&framework=13&filter_significance=significant&filter_status=improvement%2Cregression (Perf neutral)
GDrive STR:
- Patched : https://share.firefox.dev/3S09nnj (2.9s)
- Chrome: https://share.firefox.dev/4hmQfKB (3s)
testcase from comment4 (100x loopcount): https://share.firefox.dev/4bDG80j (85ms)
sparse array test (100x loopcount): https://share.firefox.dev/4q6Agmg (120ms-150ms)
Description
•