diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp index 5218938ca3..740c18417f 100644 --- a/dom/base/nsINode.cpp +++ b/dom/base/nsINode.cpp @@ -2927,6 +2927,35 @@ FindMatchingElementsWithId(const nsAString& aId, nsINode* aRoot, // Actually find elements matching aSelectorList (which must not be // null) and which are descendants of aRoot and put them in aList. If // onlyFirstMatch, then stop once the first one is found. +template +static void +FindMatchingElementsWithClass(nsINode* aRoot, nsIAtom* aClass, + nsCaseTreatment aCaseTreatment, T& aList) +{ + Collector results; + for (nsIContent* cur = aRoot->GetFirstChild(); cur; + cur = cur->GetNextNode(aRoot)) { + if (!cur->IsElement()) { + continue; + } + const nsAttrValue* classes = cur->AsElement()->GetClasses(); + if (classes && classes->Contains(aClass, aCaseTreatment)) { + if (onlyFirstMatch) { + aList.AppendElement(cur->AsElement()); + return; + } + results.AppendElement(cur->AsElement()); + } + } + const uint32_t len = results.Length(); + if (len) { + aList.SetCapacity(len); + for (uint32_t i = 0; i < len; ++i) { + aList.AppendElement(results.ElementAt(i)); + } + } +} + template MOZ_ALWAYS_INLINE static void FindMatchingElements(nsINode* aRoot, nsCSSSelectorList* aSelectorList, T &aList, @@ -2934,6 +2963,23 @@ FindMatchingElements(nsINode* aRoot, nsCSSSelectorList* aSelectorList, T &aList, { nsIDocument* doc = aRoot->OwnerDoc(); + // Parsed selectors are already cached by the document. A lone class needs + // only an atom lookup per element, not the general CSS matching context. + nsCSSSelector* selector = aSelectorList->mSelectors; + if (!aSelectorList->mNext && !selector->mNext && + selector->mClassList && !selector->mClassList->mNext && + !selector->mLowercaseTag && !selector->mIDList && + !selector->mAttrList && !selector->mPseudoClassList && + !selector->mNegations && selector->mNameSpace == kNameSpaceID_Unknown && + selector->IsRestrictedSelector() && !selector->IsHybridPseudoElement()) { + nsCaseTreatment caseTreatment = + doc->GetCompatibilityMode() == eCompatibility_NavQuirks + ? eIgnoreCase : eCaseMatters; + FindMatchingElementsWithClass( + aRoot, selector->mClassList->mAtom, caseTreatment, aList); + return; + } + TreeMatchContext matchingContext(false, nsRuleWalker::eRelevantLinkUnvisited, doc, TreeMatchContext::eNeverMatchVisited); doc->FlushPendingLinkUpdates(); diff --git a/dom/base/test/file_class_selector_checks.js b/dom/base/test/file_class_selector_checks.js new file mode 100644 index 0000000000..0be76bc6ef --- /dev/null +++ b/dom/base/test/file_class_selector_checks.js @@ -0,0 +1,59 @@ +function checkClassSelectors(parse, equal) { + function check(root, selector, expected) { + var all = root.querySelectorAll(selector); + equal(all.length, expected.length, selector + " count"); + for (var i = 0; i < expected.length; i++) { + equal(all[i].id, expected[i], selector + " order " + i); + } + equal(root.querySelector(selector), all.length ? all[0] : null, + selector + " first match"); + } + + var doc = parse('' + + '
' + + '
' + + '
' + + '' + + '
', 'text/html'); + var root = doc.getElementById('root'); + for (var repeat = 0; repeat < 10; repeat++) { + check(root, '.toggle', ['a', 'b', 'd', 'svg']); + check(root, '*|*.toggle', ['a', 'b', 'd', 'svg']); + check(root, '.t\\6f ggle', ['a', 'b', 'd', 'svg']); + check(root, '.TOGGLE', ['c']); + check(root, '.missing', []); + check(root, '.toggle.completed', ['a']); + check(root, 'button.toggle', ['d']); + check(root, '.toggle:not(.completed)', ['b', 'd', 'svg']); + check(root, '.toggle, .other', ['a', 'b', 'd', 'svg']); + } + check(doc, '.toggle', ['root', 'a', 'b', 'd', 'svg']); + + var snapshot = root.querySelectorAll('.toggle'); + doc.getElementById('a').className = ''; + root.removeChild(doc.getElementById('d')); + doc.getElementById('c').className = 'toggle'; + check(root, '.toggle', ['b', 'c', 'svg']); + equal(snapshot.length, 4, 'querySelectorAll remains a static snapshot'); + equal(snapshot[0].id, 'a', 'snapshot retains a changed element'); + equal(snapshot[2].id, 'd', 'snapshot retains a removed element'); + + var fragment = doc.createDocumentFragment(); + fragment.appendChild(root); + check(fragment, '.toggle', ['root', 'b', 'c', 'svg']); + check(root, '.toggle', ['b', 'c', 'svg']); + + var quirks = parse('
' + + '
', 'text/html'); + equal(quirks.compatMode, 'BackCompat', 'quirks document'); + check(quirks, '.toggle', ['lower', 'upper']); + check(quirks, '.TOGGLE', ['lower', 'upper']); + + var xml = parse('' + + '' + + '', + 'application/xml'); + check(xml, '.toggle', ['plain', 'namespaced']); + check(xml, '|*.toggle', ['plain']); + check(xml, '*|*.toggle', ['plain', 'namespaced']); +} diff --git a/dom/base/test/mochitest.ini b/dom/base/test/mochitest.ini index 6f5e0ea835..f903dc5a3d 100644 --- a/dom/base/test/mochitest.ini +++ b/dom/base/test/mochitest.ini @@ -1,5 +1,6 @@ [DEFAULT] support-files = + file_class_selector_checks.js audio.ogg audioEndedDuringPlaying.webm iframe_bug962251.html @@ -615,6 +616,7 @@ skip-if = os == "mac" # Different tab focus behavior on mac [test_caretPositionFromPoint.html] [test_change_policy.html] [test_classList.html] +[test_class_selector_fast_path.html] [test_clearTimeoutIntervalNoArg.html] [test_constructor-assignment.html] [test_constructor.html] diff --git a/dom/base/test/test_class_selector_fast_path.html b/dom/base/test/test_class_selector_fast_path.html new file mode 100644 index 0000000000..3eb87d2cf2 --- /dev/null +++ b/dom/base/test/test_class_selector_fast_path.html @@ -0,0 +1,10 @@ + + +Class selector query fast path + + + diff --git a/js/src/devtools/speedometer21-json-bench.js b/js/src/devtools/speedometer21-json-bench.js new file mode 100644 index 0000000000..264d7d713b --- /dev/null +++ b/js/src/devtools/speedometer21-json-bench.js @@ -0,0 +1,38 @@ +// Focused JSON workloads motivated by Speedometer 2.1's in-memory TodoMVC store. +// Run with a JS shell, or xpcshell -f. Lower times are better. This does not +// measure Speedometer's DOM, layout, event dispatch, or overall suite score. +(function() { + var iterations = 2000; + var checksum = 0; + function measure(name, records) { + var text = JSON.stringify(records); + var samples = []; + for (var i = 0; i < 100; i++) + checksum += JSON.parse(text).length; + for (var sample = 0; sample < 7; sample++) { + if (typeof gc === 'function') + gc(); + var start = Date.now(); + for (var i = 0; i < iterations; i++) + checksum += JSON.parse(text).length; + samples.push(Date.now() - start); + } + samples.sort(function(a, b) { return a - b; }); + print(name + ': median=' + samples[3] + ' ms; samples=' + samples.join(',')); + } + var todos = [], unicode = [], collisions = [], unique = []; + for (var i = 0; i < 100; i++) { + todos.push({ id: i, title: 'Something to do ' + i, completed: false }); + unicode.push({ '\u0101name': i, '\u03bbvalue': 'value', '\u4e2d': false }); + collisions.push({ item: i, identifier: i + 1, index: i + 2 }); + var record = {}; + record['uniqueName' + i] = i; + unique.push(record); + } + measure('TodoMVC-shaped records', todos); + measure('two-byte names', unicode); + measure('cache collisions', collisions); + measure('unique names', unique); + measure('small parse', [todos[0]]); + print('checksum=' + checksum); +})(); diff --git a/js/src/jit-test/tests/basic/json-property-name-cache.js b/js/src/jit-test/tests/basic/json-property-name-cache.js new file mode 100644 index 0000000000..92440c8952 --- /dev/null +++ b/js/src/jit-test/tests/basic/json-property-name-cache.js @@ -0,0 +1,43 @@ +// Repeated names, collisions, encodings, and escaped names must all agree. +var keys = ['id', 'title', 'completed', 'items', 'item', '', 'identifier', + 'i', 'a', 'same', 'samesize', '\u00e9', '\u0101', '\ud800', + 'quote"key', 'slash\\key', 'line\nkey', '__proto__']; +var records = []; +for (var i = 0; i < 100; i++) { + var record = Object.create(null); + for (var k = 0; k < keys.length; k++) + record[keys[k]] = i + k; + records.push(record); +} +var text = JSON.stringify(records); +for (var repeat = 0; repeat < 30; repeat++) { + var parsed = JSON.parse(text); + assertEq(parsed.length, records.length); + for (var i = 0; i < parsed.length; i++) { + for (var k = 0; k < keys.length; k++) + assertEq(parsed[i][keys[k]], records[i][keys[k]]); + } +} +assertEq(JSON.parse('[{"title":1},{"titleLonger":2}]')[1].titleLonger, 2); +assertEq(JSON.parse('[{"titleLonger":1},{"title":2}]')[1].title, 2); +assertEq(JSON.parse('[{"title":1},{"t\\u0069tle":2}]')[1].title, 2); +assertEq(JSON.parse('{"id":1,"id":2}').id, 2); +assertEq(JSON.parse('{"id":1}', function(key, value) { + return key === 'id' ? JSON.parse('{"id":2}').id : value; +}).id, 2); + +for (var bad of ['[{"title":1},{"title', '[{"title":1},{"title"', + '[{"title":1},{"titleX":}]', '[{"title":1},{"ti\ntle":2}]']) { + var threw = false; + try { JSON.parse(bad); } catch (e) { threw = e instanceof SyntaxError; } + assertEq(threw, true); +} + +// Keep cached atoms alive across allocations made while parsing later values. +var large = '[{"uncommonPropertyForGC":0},' + + '{"other":[' + new Array(20000).fill('"allocation"').join(',') + ']},' + + '{"uncommonPropertyForGC":1}]'; +for (var i = 0; i < 3; i++) { + gc(); + assertEq(JSON.parse(large)[2].uncommonPropertyForGC, 1); +} diff --git a/js/src/vm/JSONParser.cpp b/js/src/vm/JSONParser.cpp index 680a34fef0..2a70f9f771 100644 --- a/js/src/vm/JSONParser.cpp +++ b/js/src/vm/JSONParser.cpp @@ -43,6 +43,10 @@ JSONParserBase::~JSONParserBase() void JSONParserBase::trace(JSTracer* trc) { + for (auto& atom : propertyNameCache) { + if (atom) + TraceRoot(trc, &atom, "JSONParser cached property name"); + } for (size_t i = 0; i < stack.length(); i++) { if (stack[i].state == FinishArrayElement) { ElementVector& elements = stack[i].elements(); @@ -123,6 +127,24 @@ JSONParser::readString() return token(Error); } + // Arrays of records repeatedly use the same property names. Verify the + // entire name and closing quote before skipping scanning and atomization. + // Only the unescaped path below populates this cache. + if (ST == JSONParser::PropertyName && *current != '"') { + JSAtom* atom = propertyNameCache[size_t(*current) % PropertyNameCacheSize]; + if (atom && size_t(end - current) > atom->length() && + current[atom->length()] == '"') { + JS::AutoCheckCannotGC nogc; + bool matches = atom->hasLatin1Chars() + ? EqualChars(atom->latin1Chars(nogc), current.get(), atom->length()) + : EqualChars(atom->twoByteChars(nogc), current.get(), atom->length()); + if (matches) { + current += atom->length() + 1; + return stringToken(atom); + } + } + } + /* * Optimization: if the source contains no escaped characters, create the * string directly from the source text. @@ -137,6 +159,9 @@ JSONParser::readString() : NewStringCopyN(cx, start.get(), length); if (!str) return token(OOM); + if (ST == JSONParser::PropertyName && length) { + propertyNameCache[size_t(*start) % PropertyNameCacheSize] = &str->asAtom(); + } return stringToken(str); } diff --git a/js/src/vm/JSONParser.h b/js/src/vm/JSONParser.h index 70ed86f58e..9c3a979c69 100644 --- a/js/src/vm/JSONParser.h +++ b/js/src/vm/JSONParser.h @@ -33,6 +33,11 @@ class MOZ_STACK_CLASS JSONParserBase const ErrorHandling errorHandling; + // Reuse unescaped property names within this parse. The cache is bounded + // and traced with the parser, including during compacting GC. + static const size_t PropertyNameCacheSize = 8; + JSAtom* propertyNameCache[PropertyNameCacheSize]; + enum Token { String, Number, True, False, Null, ArrayOpen, ArrayClose, ObjectOpen, ObjectClose, @@ -109,6 +114,7 @@ class MOZ_STACK_CLASS JSONParserBase JSONParserBase(JSContext* cx, ErrorHandling errorHandling) : cx(cx), errorHandling(errorHandling), + propertyNameCache{}, stack(cx), freeElements(cx), freeProperties(cx) @@ -129,8 +135,10 @@ class MOZ_STACK_CLASS JSONParserBase #ifdef DEBUG , lastToken(mozilla::Move(other.lastToken)) #endif - {} - + { + for (size_t i = 0; i < PropertyNameCacheSize; i++) + propertyNameCache[i] = other.propertyNameCache[i]; + } Value numberValue() const { MOZ_ASSERT(lastToken == Number);