Bug 1023386 - Split and filter properties remotely for objects. r=past

This commit is contained in:
Alexandre Poirot 2015-05-25 20:11:59 +02:00
parent 208678a31d
commit 6259e01e06
6 changed files with 586 additions and 163 deletions

View File

@ -70,10 +70,7 @@ function initialChecks() {
is(objectVar.expanded, false,
"The 'largeObject' variable shouldn't be expanded.");
let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_PROPERTIES, 2);
arrayVar.expand();
objectVar.expand();
return finished;
return promise.all([arrayVar.expand(),objectVar.expand()]);
}
function verifyFirstLevel() {
@ -96,55 +93,56 @@ function verifyFirstLevel() {
"The 'largeObject' should contain all the created non-enumerable elements.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
0 + gEllipsis + 1999, "The first page in the 'largeArray' is named correctly.");
"[0" + gEllipsis + "2499]", "The first page in the 'largeArray' is named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
"", "The first page in the 'largeArray' should not have a corresponding value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
2000 + gEllipsis + 3999, "The second page in the 'largeArray' is named correctly.");
"[2500" + gEllipsis + "4999]", "The second page in the 'largeArray' is named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
"", "The second page in the 'largeArray' should not have a corresponding value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
4000 + gEllipsis + 5999, "The third page in the 'largeArray' is named correctly.");
"[5000" + gEllipsis + "7499]", "The third page in the 'largeArray' is named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"),
"", "The third page in the 'largeArray' should not have a corresponding value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
6000 + gEllipsis + 9999, "The fourth page in the 'largeArray' is named correctly.");
"[7500" + gEllipsis + "9999]", "The fourth page in the 'largeArray' is named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"),
"", "The fourth page in the 'largeArray' should not have a corresponding value.");
is(objectVar.target.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
0 + gEllipsis + 1999, "The first page in the 'largeObject' is named correctly.");
"[0" + gEllipsis + "2499]", "The first page in the 'largeObject' is named correctly.");
is(objectVar.target.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
"", "The first page in the 'largeObject' should not have a corresponding value.");
is(objectVar.target.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
2000 + gEllipsis + 3999, "The second page in the 'largeObject' is named correctly.");
"[2500" + gEllipsis + "4999]", "The second page in the 'largeObject' is named correctly.");
is(objectVar.target.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
"", "The second page in the 'largeObject' should not have a corresponding value.");
is(objectVar.target.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
4000 + gEllipsis + 5999, "The thrid page in the 'largeObject' is named correctly.");
"[5000" + gEllipsis + "7499]", "The thrid page in the 'largeObject' is named correctly.");
is(objectVar.target.querySelectorAll(".variables-view-property .value")[2].getAttribute("value"),
"", "The thrid page in the 'largeObject' should not have a corresponding value.");
is(objectVar.target.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
6000 + gEllipsis + 9999, "The fourth page in the 'largeObject' is named correctly.");
"[7500" + gEllipsis + "9999]", "The fourth page in the 'largeObject' is named correctly.");
is(objectVar.target.querySelectorAll(".variables-view-property .value")[3].getAttribute("value"),
"", "The fourth page in the 'largeObject' should not have a corresponding value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[4].getAttribute("value"),
"length", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"),
"10000", "The other properties 'largeArray' have the correct value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"),
"buffer", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"),
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[4].getAttribute("value"),
"ArrayBuffer", "The other properties 'largeArray' have the correct value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"),
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[5].getAttribute("value"),
"byteLength", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"),
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[5].getAttribute("value"),
"10000", "The other properties 'largeArray' have the correct value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[7].getAttribute("value"),
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[6].getAttribute("value"),
"byteOffset", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[7].getAttribute("value"),
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[6].getAttribute("value"),
"0", "The other properties 'largeArray' have the correct value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[7].getAttribute("value"),
"length", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[7].getAttribute("value"),
"10000", "The other properties 'largeArray' have the correct value.");
is(arrayVar.target.querySelectorAll(".variables-view-property .name")[8].getAttribute("value"),
"__proto__", "The other properties 'largeArray' are named correctly.");
is(arrayVar.target.querySelectorAll(".variables-view-property .value")[8].getAttribute("value"),
@ -160,10 +158,13 @@ function verifyNextLevels() {
let localScope = gVariables.getScopeAtIndex(0);
let objectVar = localScope.get("largeObject");
let lastPage1 = objectVar.get(6000 + gEllipsis + 9999);
let lastPage1 = objectVar.get("[7500" + gEllipsis + "9999]");
ok(lastPage1, "The last page in the first level was retrieved successfully.");
lastPage1.expand();
return lastPage1.expand()
.then(verifyNextLevels2.bind(null, lastPage1));
}
function verifyNextLevels2(lastPage1) {
let pageEnums1 = lastPage1.target.querySelector(".variables-view-element-details.enum").childNodes;
let pageNonEnums1 = lastPage1.target.querySelector(".variables-view-element-details.nonenum").childNodes;
is(pageEnums1.length, 0,
@ -172,61 +173,44 @@ function verifyNextLevels() {
"The last page in the first level should contain all the created non-enumerable elements.");
is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
6000 + gEllipsis + 6999, "The first page in this level named correctly (1).");
"[7500" + gEllipsis + "8124]", "The first page in this level named correctly (1).");
is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
7000 + gEllipsis + 7999, "The second page in this level named correctly (1).");
"[8125" + gEllipsis + "8749]", "The second page in this level named correctly (1).");
is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
8000 + gEllipsis + 8999, "The third page in this level named correctly (1).");
"[8750" + gEllipsis + "9374]", "The third page in this level named correctly (1).");
is(lastPage1._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
9000 + gEllipsis + 9999, "The fourth page in this level named correctly (1).");
"[9375" + gEllipsis + "9999]", "The fourth page in this level named correctly (1).");
let lastPage2 = lastPage1.get(9000 + gEllipsis + 9999);
let lastPage2 = lastPage1.get("[9375" + gEllipsis + "9999]");
ok(lastPage2, "The last page in the second level was retrieved successfully.");
lastPage2.expand();
return lastPage2.expand()
.then(verifyNextLevels3.bind(null, lastPage2));
}
function verifyNextLevels3(lastPage2) {
let pageEnums2 = lastPage2.target.querySelector(".variables-view-element-details.enum").childNodes;
let pageNonEnums2 = lastPage2.target.querySelector(".variables-view-element-details.nonenum").childNodes;
is(pageEnums2.length, 0,
"The last page in the second level shouldn't contain any enumerable elements.");
is(pageNonEnums2.length, 4,
"The last page in the second level should contain all the created non-enumerable elements.");
is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
9000 + gEllipsis + 9199, "The first page in this level named correctly (2).");
is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
9200 + gEllipsis + 9399, "The second page in this level named correctly (2).");
is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[2].getAttribute("value"),
9400 + gEllipsis + 9599, "The third page in this level named correctly (2).");
is(lastPage2._nonenum.querySelectorAll(".variables-view-property .name")[3].getAttribute("value"),
9600 + gEllipsis + 9999, "The fourth page in this level named correctly (2).");
let lastPage3 = lastPage2.get(9600 + gEllipsis + 9999);
ok(lastPage3, "The last page in the third level was retrieved successfully.");
lastPage3.expand();
let pageEnums3 = lastPage3.target.querySelector(".variables-view-element-details.enum").childNodes;
let pageNonEnums3 = lastPage3.target.querySelector(".variables-view-element-details.nonenum").childNodes;
is(pageEnums3.length, 400,
is(pageEnums2.length, 625,
"The last page in the third level should contain all the created enumerable elements.");
is(pageNonEnums3.length, 0,
is(pageNonEnums2.length, 0,
"The last page in the third level shouldn't contain any non-enumerable elements.");
is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
9600, "The properties in this level are named correctly (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
9601, "The properties in this level are named correctly (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[398].getAttribute("value"),
is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[0].getAttribute("value"),
9375, "The properties in this level are named correctly (3).");
is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[1].getAttribute("value"),
9376, "The properties in this level are named correctly (3).");
is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[623].getAttribute("value"),
9998, "The properties in this level are named correctly (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .name")[399].getAttribute("value"),
is(lastPage2._enum.querySelectorAll(".variables-view-property .name")[624].getAttribute("value"),
9999, "The properties in this level are named correctly (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
399, "The properties in this level have the correct value (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
398, "The properties in this level have the correct value (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[398].getAttribute("value"),
is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[0].getAttribute("value"),
624, "The properties in this level have the correct value (3).");
is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[1].getAttribute("value"),
623, "The properties in this level have the correct value (3).");
is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[623].getAttribute("value"),
1, "The properties in this level have the correct value (3).");
is(lastPage3._enum.querySelectorAll(".variables-view-property .value")[399].getAttribute("value"),
is(lastPage2._enum.querySelectorAll(".variables-view-property .value")[624].getAttribute("value"),
0, "The properties in this level have the correct value (3).");
}

View File

@ -12,7 +12,6 @@ const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
const LAZY_EMPTY_DELAY = 150; // ms
const LAZY_EXPAND_DELAY = 50; // ms
const SCROLL_PAGE_SIZE_DEFAULT = 0;
const APPEND_PAGE_SIZE_DEFAULT = 500;
const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100;
const PAGE_SIZE_MAX_JUMPS = 30;
const SEARCH_ACTION_MAX_DELAY = 300; // ms
@ -246,12 +245,6 @@ VariablesView.prototype = {
*/
scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT,
/**
* The maximum number of elements allowed in a scope, variable or property
* that allows pagination when appending children.
*/
appendPageSize: APPEND_PAGE_SIZE_DEFAULT,
/**
* Function called each time a variable or property's value is changed via
* user interaction. If null, then value changes are disabled.
@ -556,6 +549,14 @@ VariablesView.prototype = {
* The variable or property to search for.
*/
_doSearch: function(aToken) {
if (this.controller.supportsSearch()) {
this.empty();
let scope = this.addScope(aToken);
scope.expanded = true; // Expand the scope by default.
scope.locked = true; // Prevent collapsing the scope.
this.controller.performSearch(scope, aToken);
return;
}
for (let scope of this._store) {
switch (aToken) {
case "":
@ -1214,7 +1215,6 @@ function Scope(aView, aName, aFlags = {}) {
// Inherit properties and flags from the parent view. You can override
// each of these directly onto any scope, variable or property instance.
this.scrollPageSize = aView.scrollPageSize;
this.appendPageSize = aView.appendPageSize;
this.eval = aView.eval;
this.switch = aView.switch;
this.delete = aView.delete;
@ -1320,81 +1320,12 @@ Scope.prototype = {
* Additional options for adding the properties. Supported options:
* - sorted: true to sort all the properties before adding them
* - callback: function invoked after each item is added
* @param string aKeysType [optional]
* Helper argument in the case of paginated items. Can be either
* "just-strings" or "just-numbers". Humans shouldn't use this argument.
*/
addItems: function(aItems, aOptions = {}, aKeysType = "") {
addItems: function(aItems, aOptions = {}) {
let names = Object.keys(aItems);
// Building the view when inspecting an object with a very large number of
// properties may take a long time. To avoid blocking the UI, group
// the items into several lazily populated pseudo-items.
let exceedsThreshold = names.length >= this.appendPageSize;
let shouldPaginate = exceedsThreshold && aKeysType != "just-strings";
if (shouldPaginate && this.allowPaginate) {
// Group the items to append into two separate arrays, one containing
// number-like keys, the other one containing string keys.
if (aKeysType == "just-numbers") {
var numberKeys = names;
var stringKeys = [];
} else {
var numberKeys = [];
var stringKeys = [];
for (let name of names) {
// Be very careful. Avoid Infinity, NaN and non Natural number keys.
let coerced = +name;
if (Number.isInteger(coerced) && coerced > -1) {
numberKeys.push(name);
} else {
stringKeys.push(name);
}
}
}
// This object contains a very large number of properties, but they're
// almost all strings that can't be coerced to numbers. Don't paginate.
if (numberKeys.length < this.appendPageSize) {
this.addItems(aItems, aOptions, "just-strings");
return;
}
// Slices a section of the { name: descriptor } data properties.
let paginate = (aArray, aBegin = 0, aEnd = aArray.length) => {
let store = {}
for (let i = aBegin; i < aEnd; i++) {
let name = aArray[i];
store[name] = aItems[name];
}
return store;
};
// Creates a pseudo-item that populates itself with the data properties
// from the corresponding page range.
let createRangeExpander = (aArray, aBegin, aEnd, aOptions, aKeyTypes) => {
let rangeVar = this.addItem(aArray[aBegin] + Scope.ellipsis + aArray[aEnd - 1]);
rangeVar.onexpand = () => {
let pageItems = paginate(aArray, aBegin, aEnd);
rangeVar.addItems(pageItems, aOptions, aKeyTypes);
}
rangeVar.showArrow();
rangeVar.target.setAttribute("pseudo-item", "");
};
// Divide the number keys into quarters.
let page = +Math.round(numberKeys.length / 4).toPrecision(1);
createRangeExpander(numberKeys, 0, page, aOptions, "just-numbers");
createRangeExpander(numberKeys, page, page * 2, aOptions, "just-numbers");
createRangeExpander(numberKeys, page * 2, page * 3, aOptions, "just-numbers");
createRangeExpander(numberKeys, page * 3, numberKeys.length, aOptions, "just-numbers");
// Append all the string keys together.
this.addItems(paginate(stringKeys), aOptions, "just-strings");
return;
}
// Sort all of the properties before adding them, if preferred.
if (aOptions.sorted && aKeysType != "just-numbers") {
if (aOptions.sorted) {
names.sort(this._naturalSort);
}
@ -1536,7 +1467,11 @@ Scope.prototype = {
this._isExpanded = true;
if (this.onexpand) {
this.onexpand(this);
// We return onexpand as it sometimes returns a promise
// (up to the user of VariableView to do it)
// that can indicate when the view is done expanding
// and attributes are available. (Mostly used for tests)
return this.onexpand(this);
}
},

View File

@ -32,8 +32,11 @@ XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
const MAX_LONG_STRING_LENGTH = 200000;
const MAX_PROPERTY_ITEMS = 2000;
const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data
this.EXPORTED_SYMBOLS = ["VariablesViewController", "StackFrameUtils"];
@ -158,6 +161,166 @@ VariablesViewController.prototype = {
return deferred.promise;
},
/**
* Adds pseudo items in case there is too many properties to display.
* Each item can expand into property slices.
*
* @param Scope aTarget
* The Scope where the properties will be placed into.
* @param object aGrip
* The property iterator grip.
* @param object aIterator
* The property iterator client.
*/
_populatePropertySlices: function(aTarget, aGrip, aIterator) {
if (aGrip.count < MAX_PROPERTY_ITEMS) {
return this._populateFromPropertyIterator(aTarget, aGrip);
}
// Divide the keys into quarters.
let items = Math.ceil(aGrip.count / 4);
let promises = [];
for(let i = 0; i < 4; i++) {
let start = aGrip.start + i * items;
let count = i != 3 ? items : aGrip.count - i * items;
// Create a new kind of grip, with additional fields to define the slice
let sliceGrip = {
type: "property-iterator",
propertyIterator: aIterator,
start: start,
count: count
};
// Query the name of the first and last items for this slice
let deferred = promise.defer();
aIterator.names([start, start + count - 1], ({ names }) => {
let label = "[" + names[0] + ELLIPSIS + names[1] + "]";
let item = aTarget.addItem(label);
item.showArrow();
this.addExpander(item, sliceGrip);
deferred.resolve();
});
promises.push(deferred.promise);
}
return promise.all(promises);
},
/**
* Adds a property slice for a Variable in the view using the already
* property iterator
*
* @param Scope aTarget
* The Scope where the properties will be placed into.
* @param object aGrip
* The property iterator grip.
*/
_populateFromPropertyIterator: function(aTarget, aGrip) {
if (aGrip.count >= MAX_PROPERTY_ITEMS) {
// We already started to split, but there is still too many properties, split again.
return this._populatePropertySlices(aTarget, aGrip, aGrip.propertyIterator);
}
// We started slicing properties, and the slice is now small enough to be displayed
let deferred = promise.defer();
aGrip.propertyIterator.slice(aGrip.start, aGrip.count,
({ ownProperties }) => {
// Add all the variable properties.
if (Object.keys(ownProperties).length > 0) {
aTarget.addItems(ownProperties, {
sorted: true,
// Expansion handlers must be set after the properties are added.
callback: this.addExpander
});
}
deferred.resolve();
});
return deferred.promise;
},
/**
* Adds the properties for a Variable in the view using a new feature in FF40+
* that allows iteration over properties in slices.
*
* @param Scope aTarget
* The Scope where the properties will be placed into.
* @param object aGrip
* The grip to use to populate the target.
* @param string aQuery [optional]
* The query string used to fetch only a subset of properties
*/
_populateFromObjectWithIterator: function(aTarget, aGrip, aQuery) {
// FF40+ starts exposing `ownPropertyLength` on ObjectActor's grip,
// as well as `enumProperties` request.
let deferred = promise.defer();
let objectClient = this._getObjectClient(aGrip);
let isArray = aGrip.preview && aGrip.preview.kind === "ArrayLike";
if (isArray) {
// First enumerate array items, e.g. properties from `0` to `array.length`.
let options = {
ignoreNonIndexedProperties: true,
ignoreSafeGetters: true,
query: aQuery
};
objectClient.enumProperties(options, ({ iterator }) => {
let sliceGrip = {
type: "property-iterator",
propertyIterator: iterator,
start: 0,
count: iterator.count
};
this._populatePropertySlices(aTarget, sliceGrip, iterator)
.then(() => {
// Then enumerate the rest of the properties, like length, buffer, etc.
let options = {
ignoreIndexedProperties: true,
sort: true,
query: aQuery
};
objectClient.enumProperties(options, ({ iterator }) => {
let sliceGrip = {
type: "property-iterator",
propertyIterator: iterator,
start: 0,
count: iterator.count
};
deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip, iterator));
});
});
});
} else {
// For objects, we just enumerate all the properties sorted by name.
objectClient.enumProperties({ sort: true, query: aQuery }, ({ iterator }) => {
let sliceGrip = {
type: "property-iterator",
propertyIterator: iterator,
start: 0,
count: iterator.count
};
deferred.resolve(this._populatePropertySlices(aTarget, sliceGrip, iterator));
});
}
return deferred.promise;
},
/**
* Adds the given prototype in the view.
*
* @param Scope aTarget
* The Scope where the properties will be placed into.
* @param object aProtype
* The prototype grip.
*/
_populateObjectPrototype: function(aTarget, aPrototype) {
// Add the variable's __proto__.
if (aPrototype && aPrototype.type != "null") {
let proto = aTarget.addItem("__proto__", { value: aPrototype });
this.addExpander(proto, aPrototype);
}
},
/**
* Adds properties to a Scope, Variable, or Property in the view. Triggered
* when a scope is expanded or certain variables are hovered.
@ -168,7 +331,19 @@ VariablesViewController.prototype = {
* The grip to use to populate the target.
*/
_populateFromObject: function(aTarget, aGrip) {
let deferred = promise.defer();
// Fetch properties by slices if there is too many in order to prevent UI freeze.
if ("ownPropertyLength" in aGrip && aGrip.ownPropertyLength >= MAX_PROPERTY_ITEMS) {
return this._populateFromObjectWithIterator(aTarget, aGrip)
.then(() => {
let deferred = promise.defer();
let objectClient = this._getObjectClient(aGrip);
objectClient.getPrototype(({ prototype }) => {
this._populateObjectPrototype(aTarget, prototype);
deferred.resolve();
});
return deferred.promise;
});
}
if (aGrip.class === "Promise" && aGrip.promiseState) {
const { state, value, reason } = aGrip.promiseState;
@ -179,10 +354,16 @@ VariablesViewController.prototype = {
this.addExpander(aTarget.addItem("<reason>", { value: reason }), reason);
}
}
return this._populateProperties(aTarget, aGrip);
},
_populateProperties: function(aTarget, aGrip, aOptions) {
let deferred = promise.defer();
let objectClient = this._getObjectClient(aGrip);
objectClient.getPrototypeAndProperties(aResponse => {
let { ownProperties, prototype } = aResponse;
let ownProperties = aResponse.ownProperties || {};
let prototype = aResponse.prototype || null;
// 'safeGetterValues' is new and isn't necessary defined on old actors.
let safeGetterValues = aResponse.safeGetterValues || {};
let sortable = VariablesView.isSortable(aGrip.class);
@ -200,21 +381,15 @@ VariablesViewController.prototype = {
}
// Add all the variable properties.
if (ownProperties) {
aTarget.addItems(ownProperties, {
// Not all variables need to force sorted properties.
sorted: sortable,
// Expansion handlers must be set after the properties are added.
callback: this.addExpander
});
}
aTarget.addItems(ownProperties, {
// Not all variables need to force sorted properties.
sorted: sortable,
// Expansion handlers must be set after the properties are added.
callback: this.addExpander
});
// Add the variable's __proto__.
if (prototype && prototype.type != "null") {
let proto = aTarget.addItem("__proto__", { value: prototype });
// Expansion handlers must be set after the properties are added.
this.addExpander(proto, prototype);
}
this._populateObjectPrototype(aTarget, prototype);
// If the object is a function we need to fetch its scope chain
// to show them as closures for the respective function.
@ -389,6 +564,10 @@ VariablesViewController.prototype = {
let deferred = promise.defer();
aTarget._fetched = deferred.promise;
if (aSource.type === "property-iterator") {
return this._populateFromPropertyIterator(aTarget, aSource);
}
// If the target is a Variable or Property then we're fetching properties.
if (VariablesView.isVariable(aTarget)) {
this._populateFromObject(aTarget, aSource).then(() => {
@ -438,6 +617,29 @@ VariablesViewController.prototype = {
return deferred.promise;
},
/**
* Indicates to the view if the targeted actor supports properties search
*
* @return boolean True, if the actor supports enumProperty request
*/
supportsSearch: function () {
// FF40+ starts exposing ownPropertyLength on object actor's grip
// as well as enumProperty which allows to query a subset of properties.
return this.objectActor && ("ownPropertyLength" in this.objectActor);
},
/**
* Try to use the actor to perform an attribute search.
*
* @param Scope aScope
* The Scope instance to populate with properties
* @param string aToken
* The query string
*/
performSearch: function(aScope, aToken) {
this._populateFromObjectWithIterator(aScope, this.objectActor, aToken);
},
/**
* Release an actor from the controller.
*
@ -497,6 +699,8 @@ VariablesViewController.prototype = {
let populated;
if (aOptions.objectActor) {
// Save objectActor for properties filtering
this.objectActor = aOptions.objectActor;
populated = this.populate(variable, aOptions.objectActor);
variable.expand();
} else if (aOptions.rawObject) {

View File

@ -5345,6 +5345,20 @@
"n_buckets": "1000",
"description": "The time (in milliseconds) that it took a 'prototypeAndProperties' request to go round trip."
},
"DEVTOOLS_DEBUGGER_RDP_LOCAL_ENUMPROPERTIES_MS": {
"expires_in_version": "never",
"kind": "exponential",
"high": "10000",
"n_buckets": "1000",
"description": "The time (in milliseconds) that it took a 'enumProperties' request to go round trip."
},
"DEVTOOLS_DEBUGGER_RDP_REMOTE_ENUMPROPERTIES_MS": {
"expires_in_version": "never",
"kind": "exponential",
"high": "10000",
"n_buckets": "1000",
"description": "The time (in milliseconds) that it took a 'enumProperties' request to go round trip."
},
"DEVTOOLS_DEBUGGER_RDP_LOCAL_PROTOTYPESANDPROPERTIES_MS": {
"expires_in_version": "never",
"kind": "exponential",

View File

@ -2327,6 +2327,39 @@ ObjectClient.prototype = {
telemetry: "PROTOTYPEANDPROPERTIES"
}),
/**
* Request a PropertyIteratorClient instance to ease listing
* properties for this object.
*
* @param options Object
* A dictionary object with various boolean attributes:
* - ignoreSafeGetters Boolean
* If true, do not iterate over safe getters.
* - ignoreIndexedProperties Boolean
* If true, filters out Array items.
* e.g. properties names between `0` and `object.length`.
* - ignoreNonIndexedProperties Boolean
* If true, filters out items that aren't array items
* e.g. properties names that are not a number between `0`
* and `object.length`.
* - sort Boolean
* If true, the iterator will sort the properties by name
* before dispatching them.
* @param aOnResponse function Called with the client instance.
*/
enumProperties: DebuggerClient.requester({
type: "enumProperties",
options: args(0)
}, {
after: function(aResponse) {
if (aResponse.iterator) {
return { iterator: new PropertyIteratorClient(this._client, aResponse.iterator) };
}
return aResponse;
},
telemetry: "ENUMPROPERTIES"
}),
/**
* Request the property descriptor of the object's specified property.
*
@ -2380,6 +2413,74 @@ ObjectClient.prototype = {
})
};
/**
* A PropertyIteratorClient provides a way to access to property names and
* values of an object efficiently, slice by slice.
* Note that the properties can be sorted in the backend,
* this is controled while creating the PropertyIteratorClient
* from ObjectClient.enumProperties.
*
* @param aClient DebuggerClient
* The debugger client parent.
* @param aGrip Object
* A PropertyIteratorActor grip returned by the protocol via
* TabActor.enumProperties request.
*/
function PropertyIteratorClient(aClient, aGrip) {
this._grip = aGrip;
this._client = aClient;
this.request = this._client.request;
}
PropertyIteratorClient.prototype = {
get actor() { return this._grip.actor; },
/**
* Get the total number of properties available in the iterator.
*/
get count() { return this._grip.count; },
/**
* Get one or more property names that correspond to the positions in the
* indexes parameter.
*
* @param indexes Array
* An array of property indexes.
* @param aCallback Function
* The function called when we receive the property names.
*/
names: DebuggerClient.requester({
type: "names",
indexes: args(0)
}, {}),
/**
* Get a set of following property value(s).
*
* @param start Number
* The index of the first property to fetch.
* @param count Number
* The number of properties to fetch.
* @param aCallback Function
* The function called when we receive the property values.
*/
slice: DebuggerClient.requester({
type: "slice",
start: args(0),
count: args(1)
}, {}),
/**
* Get all the property values.
*
* @param aCallback Function
* The function called when we receive the property values.
*/
all: DebuggerClient.requester({
type: "all"
}, {}),
};
/**
* A LongStringClient provides a way to access "very long" strings from the
* debugger server.

View File

@ -3247,6 +3247,163 @@ let stringifiers = {
},
};
/**
* Creates an actor to iterate over an object's property names and values.
*
* @param aObjectActor ObjectActor
* The object actor.
* @param aOptions Object
* A dictionary object with various boolean attributes:
* - ignoreSafeGetters Boolean
* If true, do not iterate over safe getters.
* - ignoreIndexedProperties Boolean
* If true, filters out Array items.
* e.g. properties names between `0` and `object.length`.
* - ignoreNonIndexedProperties Boolean
* If true, filters out items that aren't array items
* e.g. properties names that are not a number between `0`
* and `object.length`.
* - sort Boolean
* If true, the iterator will sort the properties by name
* before dispatching them.
* - query String
* If non-empty, will filter the properties by names containing
* this query string. The match is not case-sensitive.
*/
function PropertyIteratorActor(aObjectActor, aOptions)
{
this.objectActor = aObjectActor;
let ownProperties = Object.create(null);
let names = [];
try {
names = this.objectActor.obj.getOwnPropertyNames();
} catch (ex) {}
let safeGetterValues = {};
let safeGetterNames = [];
if (!aOptions.ignoreSafeGetters) {
// Merge the safe getter values into the existing properties list.
safeGetterValues = this.objectActor._findSafeGetterValues(names);
safeGetterNames = Object.keys(safeGetterValues);
for (let name of safeGetterNames) {
if (names.indexOf(name) === -1) {
names.push(name);
}
}
}
if (aOptions.ignoreIndexedProperties || aOptions.ignoreNonIndexedProperties) {
let length = DevToolsUtils.getProperty(this.objectActor.obj, "length");
if (typeof(length) !== "number") {
// Pseudo arrays are flagged as ArrayLike if they have
// subsequent indexed properties without having any length attribute.
length = 0;
for (let key of names) {
if (isNaN(key) || key != length++) {
break;
}
}
}
if (aOptions.ignoreIndexedProperties) {
names = names.filter(i => {
// Use parseFloat in order to reject floats...
// (parseInt converts floats to integer)
// (Number(str) converts spaces to 0)
i = parseFloat(i);
return !Number.isInteger(i) || i < 0 || i >= length;
});
}
if (aOptions.ignoreNonIndexedProperties) {
names = names.filter(i => {
i = parseFloat(i);
return Number.isInteger(i) && i >= 0 && i < length;
});
}
}
if (aOptions.query) {
let { query } = aOptions;
query = query.toLowerCase();
names = names.filter(name => {
return name.toLowerCase().includes(query);
});
}
if (aOptions.sort) {
names.sort();
}
// Now build the descriptor list
for (let name of names) {
let desc = this.objectActor._propertyDescriptor(name);
if (!desc) {
desc = safeGetterValues[name];
}
else if (name in safeGetterValues) {
// Merge the safe getter values into the existing properties list.
let { getterValue, getterPrototypeLevel } = safeGetterValues[name];
desc.getterValue = getterValue;
desc.getterPrototypeLevel = getterPrototypeLevel;
}
ownProperties[name] = desc;
}
this.names = names;
this.ownProperties = ownProperties;
}
PropertyIteratorActor.prototype = {
actorPrefix: "propertyIterator",
grip: function () {
return {
type: "propertyIterator",
actor: this.actorID,
count: this.names.length
};
},
names: function ({ indexes }) {
let list = [];
for (let idx of indexes) {
list.push(this.names[idx]);
}
return {
names: list
};
},
slice: function ({ start, count }) {
let names = this.names.slice(start, start + count);
let props = Object.create(null);
for (let name of names) {
props[name] = this.ownProperties[name];
}
return {
ownProperties: props
};
},
all: function () {
return {
ownProperties: this.ownProperties
};
}
};
PropertyIteratorActor.prototype.requestTypes = {
"names": PropertyIteratorActor.prototype.names,
"slice": PropertyIteratorActor.prototype.slice,
"all": PropertyIteratorActor.prototype.all,
};
exports.PropertyIteratorActor = PropertyIteratorActor;
/**
* Creates an actor for the specified object.
*
@ -3260,6 +3417,7 @@ function ObjectActor(aObj, aThreadActor)
dbg_assert(!aObj.optimizedOut, "Should not create object actors for optimized out values!");
this.obj = aObj;
this.threadActor = aThreadActor;
this.iterators = new Set();
}
ObjectActor.prototype = {
@ -3292,6 +3450,16 @@ ObjectActor.prototype = {
}
}
// FF40+: Allow to know how many properties an object has
// to lazily display them when there is a bunch.
// Throws on some MouseEvent object in tests.
try {
// Bug 1163520: Assert on internal functions
if (this.obj.class != "Function") {
g.ownPropertyLength = this.obj.getOwnPropertyNames().length;
}
} catch(e) {}
let raw = this.obj.unsafeDereference();
// If Cu is not defined, we are running on a worker thread, where xrays
@ -3328,6 +3496,8 @@ ObjectActor.prototype = {
if (this.registeredPool.objectActors) {
this.registeredPool.objectActors.delete(this.obj);
}
this.iterators.forEach(actor => this.registeredPool.removeActor(actor));
this.iterators.clear();
this.registeredPool.removeActor(this);
},
@ -3380,6 +3550,20 @@ ObjectActor.prototype = {
ownPropertyNames: this.obj.getOwnPropertyNames() };
},
/**
* Creates an actor to iterate over an object property names and values.
* See PropertyIteratorActor constructor for more info about options param.
*
* @param aRequest object
* The protocol request object.
*/
onEnumProperties: function (aRequest) {
let actor = new PropertyIteratorActor(this, aRequest.options);
this.registeredPool.addActor(actor);
this.iterators.add(actor);
return { iterator: actor.grip() };
},
/**
* Handle a protocol request to provide the prototype and own properties of
* the object.
@ -3406,15 +3590,15 @@ ObjectActor.prototype = {
return { from: this.actorID,
prototype: this.threadActor.createValueGrip(this.obj.proto),
ownProperties: ownProperties,
safeGetterValues: this._findSafeGetterValues(ownProperties) };
safeGetterValues: this._findSafeGetterValues(names) };
},
/**
* Find the safe getter values for the current Debugger.Object, |this.obj|.
*
* @private
* @param object aOwnProperties
* The object that holds the list of known ownProperties for
* @param array aOwnProperties
* The array that holds the list of known ownProperties names for
* |this.obj|.
* @param number [aLimit=0]
* Optional limit of getter values to find.
@ -3435,7 +3619,7 @@ ObjectActor.prototype = {
// avoid providing safeGetterValues from prototypes if property |name|
// is already defined as an own property.
if (name in safeGetterValues ||
(obj != this.obj && name in aOwnProperties)) {
(obj != this.obj && aOwnProperties.indexOf(name) !== -1)) {
continue;
}
@ -3702,6 +3886,7 @@ ObjectActor.prototype.requestTypes = {
"definitionSite": ObjectActor.prototype.onDefinitionSite,
"parameterNames": ObjectActor.prototype.onParameterNames,
"prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties,
"enumProperties": ObjectActor.prototype.onEnumProperties,
"prototype": ObjectActor.prototype.onPrototype,
"property": ObjectActor.prototype.onProperty,
"displayString": ObjectActor.prototype.onDisplayString,
@ -4423,7 +4608,7 @@ DebuggerServer.ObjectActorPreviewers.Object = [
if (i < OBJECT_PREVIEW_MAX_ITEMS) {
preview.safeGetterValues = aObjectActor.
_findSafeGetterValues(preview.ownProperties,
_findSafeGetterValues(Object.keys(preview.ownProperties),
OBJECT_PREVIEW_MAX_ITEMS - i);
}