diff --git a/README.md b/README.md
index 2c69459f92..bce6376429 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-Official Discord server
+Official Discord server
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/dom/console/ConsoleAPIStorage.js b/dom/console/ConsoleAPIStorage.js
index 31be449e91..b17c6855ec 100644
--- a/dom/console/ConsoleAPIStorage.js
+++ b/dom/console/ConsoleAPIStorage.js
@@ -94,13 +94,24 @@ ConsoleAPIStorageService.prototype = {
getEvents: function CS_getEvents(aId)
{
if (aId != null) {
- return (_consoleStorage.get(aId) || []).slice(0);
+ let storage = _consoleStorage.get(aId);
+ if (!storage) {
+ return [];
+ }
+ let { events, next } = storage;
+ return next === 0 ? events.slice() :
+ events.slice(next).concat(events.slice(0, next));
}
let result = [];
- for (let [id, events] of _consoleStorage) {
- result.push.apply(result, events);
+ for (let { events, next } of _consoleStorage.values()) {
+ for (let i = next; i < events.length; i++) {
+ result.push(events[i]);
+ }
+ for (let i = 0; i < next; i++) {
+ result.push(events[i]);
+ }
}
return result.sort(function(a, b) {
@@ -122,16 +133,21 @@ ConsoleAPIStorageService.prototype = {
*/
recordEvent: function CS_recordEvent(aId, aOuterId, aEvent)
{
- if (!_consoleStorage.has(aId)) {
- _consoleStorage.set(aId, []);
+ let storage = _consoleStorage.get(aId);
+ if (!storage) {
+ storage = { events: [], next: 0 };
+ _consoleStorage.set(aId, storage);
}
- let storage = _consoleStorage.get(aId);
- storage.push(aEvent);
-
- // truncate
- if (storage.length > STORAGE_MAX_EVENTS) {
- storage.shift();
+ // Overwrite the oldest event once full, without moving the other entries.
+ // Advance before notifying observers, which may read or reenter storage.
+ if (storage.events.length < STORAGE_MAX_EVENTS) {
+ storage.events.push(aEvent);
+ } else {
+ storage.events[storage.next] = aEvent;
+ if (++storage.next === STORAGE_MAX_EVENTS) {
+ storage.next = 0;
+ }
}
Services.obs.notifyObservers(aEvent, "console-api-log-event", aOuterId);
diff --git a/dom/console/moz.build b/dom/console/moz.build
index d18b089626..ce2ae8d054 100644
--- a/dom/console/moz.build
+++ b/dom/console/moz.build
@@ -40,5 +40,6 @@ LOCAL_INCLUDES += [
MOCHITEST_MANIFESTS += [ 'tests/mochitest.ini' ]
MOCHITEST_CHROME_MANIFESTS += [ 'tests/chrome.ini' ]
+XPCSHELL_TESTS_MANIFESTS += [ 'tests/xpcshell.ini' ]
FINAL_LIBRARY = 'xul'
diff --git a/dom/console/tests/test_console_storage_ring.js b/dom/console/tests/test_console_storage_ring.js
new file mode 100644
index 0000000000..a4e3dbeee8
--- /dev/null
+++ b/dom/console/tests/test_console_storage_ring.js
@@ -0,0 +1,53 @@
+function run_test() {
+ const storage = Components.classes["@mozilla.org/consoleAPI-storage;1"]
+ .getService(Components.interfaces.nsIConsoleAPIStorage);
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ storage.clearEvents();
+ try {
+ for (let i = 0; i < 3501; i++) {
+ storage.recordEvent("ring-test", "outer", { timeStamp: i });
+ if (i === 998 || i === 999 || i === 1000 || i === 1999 || i === 3500) {
+ let events = storage.getEvents("ring-test");
+ equal(events.length, Math.min(i + 1, 1000));
+ for (let j = 0; j < events.length; j++) {
+ equal(events[j].timeStamp, i + 1 - events.length + j);
+ }
+ events.length = 0;
+ equal(storage.getEvents("ring-test").length, Math.min(i + 1, 1000));
+ }
+ }
+ storage.recordEvent("other", "outer", { timeStamp: 2500.5 });
+ let all = storage.getEvents();
+ equal(all.length, 1001);
+ equal(all[0].timeStamp, 2500.5);
+ equal(all[1].timeStamp, 2501);
+ equal(all[1000].timeStamp, 3500);
+
+ let reentered = false;
+ let observer = {
+ observe(subject, topic, data) {
+ if (data === "ring-test" && !reentered) {
+ equal(storage.getEvents(data)[999].timeStamp, 3501);
+ reentered = true;
+ storage.recordEvent("ring-test", "outer", { timeStamp: 3502 });
+ }
+ }
+ };
+ Services.obs.addObserver(observer, "console-storage-cache-event", false);
+ try {
+ storage.recordEvent("ring-test", "outer", { timeStamp: 3501 });
+ } finally {
+ Services.obs.removeObserver(observer, "console-storage-cache-event");
+ }
+ equal(reentered, true);
+ equal(storage.getEvents("ring-test")[999].timeStamp, 3502);
+ storage.clearEvents("ring-test");
+ equal(storage.getEvents("ring-test").length, 0);
+ equal(storage.getEvents("other").length, 1);
+ storage.recordEvent("ring-test", "outer", { timeStamp: 4000 });
+ equal(storage.getEvents("ring-test")[0].timeStamp, 4000);
+ } finally {
+ storage.clearEvents();
+ }
+ equal(storage.getEvents().length, 0);
+}
diff --git a/dom/console/tests/xpcshell.ini b/dom/console/tests/xpcshell.ini
new file mode 100644
index 0000000000..7d3af0c96c
--- /dev/null
+++ b/dom/console/tests/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head =
+tail =
+
+[test_console_storage_ring.js]
diff --git a/js/src/builtin/Map.js b/js/src/builtin/Map.js
index bcf0ded02e..8c5afa9f5e 100644
--- a/js/src/builtin/Map.js
+++ b/js/src/builtin/Map.js
@@ -81,9 +81,10 @@ function MapGroupBy(items, callbackfn) {
var key = callContentFunction(callbackfn, undefined, value, k);
// Steps 6.c-d.
- var elements;
- if (callFunction(std_Map_has, groups, key)) {
- elements = callFunction(std_Map_get, groups, key);
+ // Group values are always arrays, so undefined also tells us whether
+ // this is a new key without a second hash-table lookup.
+ var elements = callFunction(std_Map_get, groups, key);
+ if (elements !== undefined) {
callFunction(std_Array_push, elements, value);
} else {
elements = [value];
diff --git a/js/src/devtools/number-to-string-bench.js b/js/src/devtools/number-to-string-bench.js
new file mode 100644
index 0000000000..957183d31d
--- /dev/null
+++ b/js/src/devtools/number-to-string-bench.js
@@ -0,0 +1,61 @@
+// Run with: js number-to-string-bench.js
+// Also supports: xpcshell -f number-to-string-bench.js
+// Compare identical optimized builds, alternating baseline and patched runs.
+// Reports milliseconds (lower is better); this is not a browser-suite score.
+
+(function() {
+ "use strict";
+ var iterations = 500000;
+ var samples = 7;
+ var checksum = 0;
+
+ function convert(values, radix, count) {
+ var total = 0;
+ for (var i = 0; i < count; i++)
+ total += values[i % values.length].toString(radix).length;
+ return total;
+ }
+
+ function unique(count) {
+ var total = 0;
+ for (var i = 0; i < count; i++)
+ total += (123456.125 + i).toString().length;
+ return total;
+ }
+
+ function measure(name, run) {
+ checksum += run(20000);
+ var times = [];
+ var expected = run(iterations);
+ for (var sample = 0; sample < samples; sample++) {
+ if (typeof gc === "function")
+ gc();
+ var start = Date.now();
+ var result = run(iterations);
+ times.push(Date.now() - start);
+ if (result !== expected)
+ throw new Error("inconsistent conversion: " + name);
+ checksum += result;
+ }
+ times.sort(function(a, b) { return a - b; });
+ print(name + ": median=" + times[3] + " ms; samples=" + times.join(","));
+ }
+
+ for (var size of [1, 2, 4, 5, 16]) {
+ var values = [];
+ for (var i = 0; i < size; i++)
+ values.push(123456.125 + i);
+ measure("decimal working set " + size, function(count) {
+ return convert(values, 10, count);
+ });
+ }
+ measure("unique decimals", unique);
+ var integers = [123456, 654321, 123457, 654322];
+ measure("integer working set 4", function(count) {
+ return convert(integers, 10, count);
+ });
+ measure("hexadecimal working set 4", function(count) {
+ return convert(integers, 16, count);
+ });
+ print("checksum=" + checksum);
+})();
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/jit-test/tests/collections/Map-groupBy-lookup.js b/js/src/jit-test/tests/collections/Map-groupBy-lookup.js
new file mode 100644
index 0000000000..945bb28b36
--- /dev/null
+++ b/js/src/jit-test/tests/collections/Map-groupBy-lookup.js
@@ -0,0 +1,47 @@
+// Repeated keys must reuse their group, including SameValueZero keys.
+var objectKey = {};
+var symbolKey = Symbol();
+var keys = [undefined, null, false, 0, -0, NaN, "key", objectKey, symbolKey];
+for (var iteration = 0; iteration < 100; iteration++) {
+ var input = [];
+ for (var repeat = 0; repeat < 4; repeat++) {
+ for (var key of keys)
+ input.push(key);
+ }
+ var calls = 0;
+ var groups = Map.groupBy(input, function(value, index) {
+ assertEq(index, calls++);
+ return value;
+ });
+ assertEq(calls, input.length);
+ assertEq(groups.size, 8);
+ for (var key of keys)
+ assertEq(groups.get(key).length, key === 0 ? 8 : 4);
+ assertEq(groups.get(objectKey)[0], objectKey);
+ assertEq(groups.get(symbolKey)[0], symbolKey);
+ assertEq(groups.get(undefined)[0], undefined);
+ var order = Array.from(groups.keys());
+ assertEq(order[0], undefined);
+ assertEq(order[3], 0);
+ assertEq(order[4], NaN);
+ assertEq(order[7], symbolKey);
+}
+
+// A throwing callback must still close the input iterator.
+var closed = false;
+function* values() {
+ try {
+ yield 1;
+ yield 2;
+ } finally {
+ closed = true;
+ }
+}
+var sentinel = {};
+try {
+ Map.groupBy(values(), function() { throw sentinel; });
+ throw new Error("callback did not throw");
+} catch (error) {
+ assertEq(error, sentinel);
+}
+assertEq(closed, true);
diff --git a/js/src/jsapi-tests/testIndexToString.cpp b/js/src/jsapi-tests/testIndexToString.cpp
index 0c91d98873..58c854054b 100644
--- a/js/src/jsapi-tests/testIndexToString.cpp
+++ b/js/src/jsapi-tests/testIndexToString.cpp
@@ -66,6 +66,56 @@ BEGIN_TEST(testIndexToString)
}
END_TEST(testIndexToString)
+BEGIN_TEST(testDtoaCacheInterleaved)
+{
+ JS::RootedString first(cx, js::NumberToString(cx, 1234.5));
+ CHECK(first);
+ JS::RootedString second(cx, js::NumberToString(cx, 6789.5));
+ CHECK(second);
+ JS::RootedString third(cx, js::IndexToString(cx, 123456));
+ CHECK(third);
+ JS::RootedString fourth(cx, js::IndexToString(cx, 654321));
+ CHECK(fourth);
+
+ for (size_t i = 0; i < 10; i++) {
+ CHECK(js::NumberToString(cx, 1234.5) == first);
+ CHECK(js::NumberToString(cx, 6789.5) == second);
+ CHECK(js::IndexToString(cx, 123456) == third);
+ CHECK(js::IndexToString(cx, 654321) == fourth);
+ }
+
+ // Every raw string pointer must be invalidated, not just the latest one.
+ JS_GC(cx);
+ CHECK(!cx->compartment()->dtoaCache.lookup(10, 1234.5));
+ CHECK(!cx->compartment()->dtoaCache.lookup(10, 6789.5));
+ CHECK(!cx->compartment()->dtoaCache.lookup(10, 123456));
+ CHECK(!cx->compartment()->dtoaCache.lookup(10, 654321));
+
+ // The radix is part of the key. Signed zero can share its string.
+ js::DtoaCache cache;
+ cache.cache(10, 0.0, &first->asFlat());
+ cache.cache(16, 0.0, &second->asFlat());
+ CHECK(cache.lookup(10, -0.0) == first);
+ CHECK(cache.lookup(16, -0.0) == second);
+ CHECK(!cache.lookup(2, 0.0));
+ cache.purge();
+ CHECK(!cache.lookup(10, 0.0));
+ CHECK(!cache.lookup(16, 0.0));
+
+ for (size_t i = 0; i < 12; i++)
+ cache.cache(10, double(i), &first->asFlat());
+ for (size_t i = 0; i < 8; i++)
+ CHECK(!cache.lookup(10, double(i)));
+ for (size_t i = 8; i < 12; i++)
+ CHECK(cache.lookup(10, double(i)) == first);
+
+ // A failed string allocation must never turn into a cache hit.
+ cache.cache(10, 12.0, nullptr);
+ CHECK(!cache.lookup(10, 12.0));
+ return true;
+}
+END_TEST(testDtoaCacheInterleaved)
+
BEGIN_TEST(testStringIsIndex)
{
for (size_t i = 0, sz = ArrayLength(tests); i < sz; i++) {
diff --git a/js/src/jscompartment.cpp b/js/src/jscompartment.cpp
index 926c7e3c86..6778dd2ebe 100644
--- a/js/src/jscompartment.cpp
+++ b/js/src/jscompartment.cpp
@@ -214,7 +214,9 @@ JSCompartment::ensureJitCompartmentExists(JSContext* cx)
void
js::DtoaCache::checkCacheAfterMovingGC()
{
- MOZ_ASSERT(!s || !IsForwarded(s));
+ MOZ_ASSERT(!recent.s || !IsForwarded(recent.s));
+ for (const auto& entry : previous)
+ MOZ_ASSERT(!entry.s || !IsForwarded(entry.s));
}
namespace {
diff --git a/js/src/jscompartment.h b/js/src/jscompartment.h
index a02b39301d..dd356aa41a 100644
--- a/js/src/jscompartment.h
+++ b/js/src/jscompartment.h
@@ -38,29 +38,47 @@ class ScriptSourceObject;
struct NativeIterator;
/*
- * A single-entry cache for some base-10 double-to-string conversions. This
- * helps date-format-xparb.js. It also avoids skewing the results for
- * v8-splay.js when measured by the SunSpider harness, where the splay tree
- * initialization (which includes many repeated double-to-string conversions)
- * is erroneously included in the measurement; see bug 562553.
+ * A small cache for number-to-string conversions, keyed by number and radix.
+ * Keep several results so interleaved conversions do not evict each other.
+ * These strings are not traced: every entry must be cleared before GC.
*/
class DtoaCache {
- double d;
- int base;
- JSFlatString* s; // if s==nullptr, d and base are not valid
+ struct Entry {
+ double d;
+ int base;
+ JSFlatString* s; // if s == nullptr, d and base are not valid
+ };
+ static const size_t NumPrevious = 3;
+ Entry recent;
+ Entry previous[NumPrevious];
+ size_t next;
public:
- DtoaCache() : s(nullptr) {}
- void purge() { s = nullptr; }
+ DtoaCache() { purge(); }
+ void purge() {
+ recent.s = nullptr;
+ for (auto& entry : previous)
+ entry.s = nullptr;
+ next = 0;
+ }
JSFlatString* lookup(int base, double d) {
- return this->s && base == this->base && d == this->d ? this->s : nullptr;
+ // Preserve the cheap path for consecutive conversions of one value.
+ if (recent.s && base == recent.base && d == recent.d)
+ return recent.s;
+ for (const auto& entry : previous) {
+ if (entry.s && base == entry.base && d == entry.d)
+ return entry.s;
+ }
+ return nullptr;
}
void cache(int base, double d, JSFlatString* s) {
- this->base = base;
- this->d = d;
- this->s = s;
+ if (recent.s) {
+ previous[next] = recent;
+ next = (next + 1) % NumPrevious;
+ }
+ recent = { d, base, s };
}
#ifdef JSGC_HASH_TABLE_CHECKS
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);