Optimize searching further

There are now 3 code paths in decreasing speed:

First, optimize subsearching more by just hiding the actors
that didn't match, since we know the ordering has to be right.

For initiating a search (or backspacing an existing one), again
instead of destroying and recreating actors, just temporarily
remove them and re-add them in the desired order.

Finally for when data has changed, use the old code path of
destroying all actors.  (This itself could obviously be optimized
if we had a way to know that just one application changed, but
at the moment we don't).

https://bugzilla.gnome.org/show_bug.cgi?id=596119
This commit is contained in:
Colin Walters 2009-09-24 18:36:36 -04:00
parent 1da4837d98
commit 159081dcfc
3 changed files with 142 additions and 103 deletions

View File

@ -197,17 +197,17 @@ AppDisplay.prototype = {
this._appsStale = true; this._appsStale = true;
this._appSystem.connect('installed-changed', Lang.bind(this, function(appSys) { this._appSystem.connect('installed-changed', Lang.bind(this, function(appSys) {
this._appsStale = true; this._appsStale = true;
this._redisplay(false); this._redisplay(0);
this._redisplayMenus(); this._redisplayMenus();
})); }));
this._appSystem.connect('favorites-changed', Lang.bind(this, function(appSys) { this._appSystem.connect('favorites-changed', Lang.bind(this, function(appSys) {
this._redisplay(false); this._redisplay(0);
})); }));
this._appMonitor.connect('app-added', Lang.bind(this, function(monitor) { this._appMonitor.connect('app-added', Lang.bind(this, function(monitor) {
this._redisplay(false); this._redisplay(0);
})); }));
this._appMonitor.connect('app-removed', Lang.bind(this, function(monitor) { this._appMonitor.connect('app-removed', Lang.bind(this, function(monitor) {
this._redisplay(false); this._redisplay(0);
})); }));
// Load the apps now so it doesn't slow down the first // Load the apps now so it doesn't slow down the first
@ -338,7 +338,7 @@ AppDisplay.prototype = {
// Gets information about all applications by calling Gio.app_info_get_all(). // Gets information about all applications by calling Gio.app_info_get_all().
_refreshCache : function() { _refreshCache : function() {
if (!this._appsStale) if (!this._appsStale)
return; return true;
this._allItems = {}; this._allItems = {};
this._appCategories = {}; this._appCategories = {};
@ -371,11 +371,13 @@ AppDisplay.prototype = {
} }
this._appsStale = false; this._appsStale = false;
return false;
}, },
// Stub this out; the app display always has a category selected // Stub this out; the app display always has a category selected
_setDefaultList : function() { _setDefaultList : function() {
this._matchedItems = []; this._matchedItems = {};
this._matchedItemKeys = [];
}, },
// Compares items associated with the item ids based on the alphabetical order // Compares items associated with the item ids based on the alphabetical order

View File

@ -150,9 +150,10 @@ DocDisplay.prototype = {
// Gets the list of recent items from the recent items manager. // Gets the list of recent items from the recent items manager.
_refreshCache : function() { _refreshCache : function() {
if (!this._docsStale) if (!this._docsStale)
return; return true;
this._allItems = this._docManager.getItems(); this._allItems = this._docManager.getItems();
this._docsStale = false; this._docsStale = false;
return false;
}, },
// Sets the list of the displayed items based on how recently they were last visited. // Sets the list of the displayed items based on how recently they were last visited.
@ -171,21 +172,24 @@ DocDisplay.prototype = {
// them once when they are returned by this._recentManager.get_items() to avoid having to do // them once when they are returned by this._recentManager.get_items() to avoid having to do
// this sorting each time, but the sorting seems to be very fast anyway, so there is no need // this sorting each time, but the sorting seems to be very fast anyway, so there is no need
// to introduce an additional class variable. // to introduce an additional class variable.
this._matchedItems = []; this._matchedItems = {};
this._matchedItemKeys = [];
let docIdsToRemove = []; let docIdsToRemove = [];
for (docId in this._allItems) { for (docId in this._allItems) {
// this._allItems[docId].exists() checks if the resource still exists // this._allItems[docId].exists() checks if the resource still exists
if (this._allItems[docId].exists()) if (this._allItems[docId].exists()) {
this._matchedItems.push(docId); this._matchedItems[docId] = 1;
else this._matchedItemKeys.push(docId);
} else {
docIdsToRemove.push(docId); docIdsToRemove.push(docId);
} }
}
for (docId in docIdsToRemove) { for (docId in docIdsToRemove) {
delete this._allItems[docId]; delete this._allItems[docId];
} }
this._matchedItems.sort(Lang.bind(this, function (a,b) { return this._compareItems(a,b); })); this._matchedItemKeys.sort(Lang.bind(this, this._compareItems));
}, },
// Compares items associated with the item ids based on how recently the items // Compares items associated with the item ids based on how recently the items

View File

@ -19,7 +19,8 @@ const Main = imports.ui.main;
const RedisplayFlags = { NONE: 0, const RedisplayFlags = { NONE: 0,
RESET_CONTROLS: 1 << 0, RESET_CONTROLS: 1 << 0,
SUBSEARCH: 1 << 1 }; FULL: 1 << 1,
SUBSEARCH: 1 << 2 };
const ITEM_DISPLAY_NAME_COLOR = new Clutter.Color(); const ITEM_DISPLAY_NAME_COLOR = new Clutter.Color();
ITEM_DISPLAY_NAME_COLOR.from_pixel(0xffffffff); ITEM_DISPLAY_NAME_COLOR.from_pixel(0xffffffff);
@ -125,6 +126,8 @@ GenericDisplayItem.prototype = {
this._description = null; this._description = null;
this._icon = null; this._icon = null;
this._initialLoadComplete = false;
// An array of details description actors that we create over time for the item. // An array of details description actors that we create over time for the item.
// It is used for updating the description text inside the details actor when // It is used for updating the description text inside the details actor when
// the description text for the item is updated. // the description text for the item is updated.
@ -341,9 +344,10 @@ GenericDisplay.prototype = {
// map<itemId, Object> where Object represents the item info // map<itemId, Object> where Object represents the item info
this._allItems = {}; this._allItems = {};
// an array of itemIds of items that match the current request // set<itemId>
// in the order in which the items should be displayed this._matchedItems = {};
this._matchedItems = []; // sorted array of items matched by search
this._matchedItemKeys = [];
// map<itemId, GenericDisplayItem> // map<itemId, GenericDisplayItem>
this._displayedItems = {}; this._displayedItems = {};
this._openDetailIndex = -1; this._openDetailIndex = -1;
@ -362,6 +366,8 @@ GenericDisplay.prototype = {
// Sets the search string and displays the matching items. // Sets the search string and displays the matching items.
setSearch: function(text) { setSearch: function(text) {
let lowertext = text.toLowerCase(); let lowertext = text.toLowerCase();
if (lowertext == this._search)
return;
let flags = RedisplayFlags.RESET_CONTROLS; let flags = RedisplayFlags.RESET_CONTROLS;
if (this._search != '') { if (this._search != '') {
if (lowertext.indexOf(this._search) == 0) if (lowertext.indexOf(this._search) == 0)
@ -440,16 +446,16 @@ GenericDisplay.prototype = {
// positive number when this._mathedItems.length is 0 // positive number when this._mathedItems.length is 0
// This can be triggered if a search string is entered for which there are no matches. // This can be triggered if a search string is entered for which there are no matches.
// log("this._mathedItems.length: " + this._matchedItems.length + " this._list.displayedCount " + this._list.displayedCount); // log("this._mathedItems.length: " + this._matchedItems.length + " this._list.displayedCount " + this._list.displayedCount);
return this._matchedItems.length > 0; return this._matchedItemKeys.length > 0;
}, },
getMatchedItemsCount: function() { getMatchedItemsCount: function() {
return this._matchedItems.length; return this._matchedItemKeys.length;
}, },
// Load the initial state // Load the initial state
load: function() { load: function() {
this._redisplay(RedisplayFlags.RESET_CONTROLS); this._redisplay(RedisplayFlags.FULL);
}, },
// Should be called when the display is closed // Should be called when the display is closed
@ -480,48 +486,11 @@ GenericDisplay.prototype = {
//// Protected methods //// //// Protected methods ////
/* _redisplayFull: function() {
* Displays items that match the current request and should show up on the current page.
* Updates the display control to reflect the matched items set and the page selected.
*
* resetDisplayControl - indicates if the display control should be re-created because
* the results or the space allocated for them changed. If it's false,
* the existing display control is used and only the page links are
* updated to reflect the current page selection.
*/
_displayMatchedItems: function(resetDisplayControl) {
// When generating a new list to display, we first remove all the old
// displayed items which will unset the selection. So we need
// to keep a flag which indicates if this display had the selection.
let hadSelected = this.hasSelected();
this._removeAllDisplayItems(); this._removeAllDisplayItems();
for (let i = 0; i < this._matchedItems.length; i++) { for (let itemId in this._allItems) {
this._addDisplayItem(this._matchedItems[i]); this._addDisplayItem(itemId);
} }
if (hadSelected) {
this._selectedIndex = -1;
this.selectFirstItem();
}
// Check if the pointer is over one of the items and display the information button if it is.
// We want to do this between finishing our changes to the display and the point where
// the display is redrawn.
Mainloop.idle_add(Lang.bind(this,
function() {
let [child, x, y, mask] = Gdk.Screen.get_default().get_root_window().get_pointer();
let actor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE,
x, y);
if (actor != null) {
let item = this._findDisplayedByActor(actor);
if (item != null) {
item.onDrawnUnderPointer();
}
}
return false;
}),
Meta.PRIORITY_BEFORE_REDRAW);
}, },
// Creates a display item based on the information associated with itemId // Creates a display item based on the information associated with itemId
@ -595,6 +564,67 @@ GenericDisplay.prototype = {
this.unsetSelected(); this.unsetSelected();
}, },
_compareSearchMatch: function(a, b) {
let countA = this._matchedItems[a];
let countB = this._matchedItems[b];
if (countA > countB)
return -1;
else if (countA < countB)
return 1;
else
return this._compareItems(a, b);
},
_setMatches: function(matches) {
this._matchedItems = matches;
this._matchedItemKeys = [];
for (let itemId in this._matchedItems) {
this._matchedItemKeys.push(itemId);
}
this._matchedItemKeys.sort(Lang.bind(this, this._compareSearchMatch));
},
/**
* _redisplaySubSearch:
* A somewhat more optimized function called when we know
* that we're going to be displaying a subset of the items
* we already had, in the same order. In that case, we can
* just hide the actors that shouldn't be shown.
*/
_redisplaySubSearch: function() {
let matches = this._getSearchMatchedItems(true);
// Just hide all from the old set,
// we'll show the ones we want below
for (let itemId in this._displayedItems) {
let item = this._displayedItems[itemId];
item.actor.hide();
}
this._setMatches(matches);
for (let itemId in matches) {
let item = this._displayedItems[itemId];
item.actor.show();
}
this._list.queue_relayout();
},
_redisplayReordering: function() {
if (!this._filterActive()) {
this._setDefaultList();
} else {
this._setMatches(this._getSearchMatchedItems(false));
}
this._list.remove_all();
for (let i = 0; i < this._matchedItemKeys.length; i++) {
let itemId = this._matchedItemKeys[i];
let item = this._displayedItems[itemId];
item.actor.show();
this._list.add_actor(item.actor);
}
},
/* /*
* Updates the displayed items, applying the search string if one exists. * Updates the displayed items, applying the search string if one exists.
* @flags: Flags controlling redisplay behavior as follows: * @flags: Flags controlling redisplay behavior as follows:
@ -607,25 +637,58 @@ GenericDisplay.prototype = {
* one, which implies we only need to re-search through previous results. * one, which implies we only need to re-search through previous results.
*/ */
_redisplay: function(flags) { _redisplay: function(flags) {
let resetPage = flags & RedisplayFlags.RESET_CONTROLS; let resetPage = (flags & RedisplayFlags.RESET_CONTROLS) > 0;
let isSubSearch = flags & RedisplayFlags.SUBSEARCH; let isSubSearch = (flags & RedisplayFlags.SUBSEARCH) > 0;
this._refreshCache(); let fullReload = (flags & RedisplayFlags.FULL) > 0;
if (!this._filterActive())
this._setDefaultList(); let hadSelected = this.hasSelected();
else
this._doSearchFilter(isSubSearch); if (!this._initialLoadComplete || !this._refreshCache())
fullReload = true;
if (fullReload) {
this._initialLoadComplete = true;
this._redisplayFull();
} if (isSubSearch) {
this._redisplaySubSearch();
} else {
this._redisplayReordering();
}
if (resetPage) if (resetPage)
this._list.page = 0; this._list.page = 0;
this._displayMatchedItems(true); if (hadSelected) {
this._selectedIndex = -1;
this.selectFirstItem();
}
Mainloop.idle_add(Lang.bind(this, this._checkInformationIcon),
Meta.PRIORITY_BEFORE_REDRAW);
this.emit('redisplayed'); this.emit('redisplayed');
}, },
// Check if the pointer is over one of the items and display the information button if it is.
// We want to do this between finishing our changes to the display and the point where
// the display is redrawn.
_checkInformationIcon: function() {
let [child, x, y, mask] = Gdk.Screen.get_default().get_root_window().get_pointer();
let actor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE,
x, y);
if (actor != null) {
let item = this._findDisplayedByActor(actor);
if (item != null) {
item.onDrawnUnderPointer();
}
}
return false;
},
//// Pure virtual protected methods //// //// Pure virtual protected methods ////
// Performs the steps needed to have the latest information about the items. // Performs the steps needed to have the latest information about the items.
// Implementation should return %true if we are up to date, and %false
// if a full reload occurred.
_refreshCache: function() { _refreshCache: function() {
throw new Error("Not implemented"); throw new Error("Not implemented");
}, },
@ -677,14 +740,14 @@ GenericDisplay.prototype = {
let matchScores = {}; let matchScores = {};
if (isSubSearch) { if (isSubSearch) {
for (let i = 0; i < this._matchedItems.length; i++) { for (let i = 0; i < this._matchedItemKeys.length; i++) {
let itemId = this._matchedItems[i]; let itemId = this._matchedItemKeys[i];
let score = this._getItemSearchScore(itemId, terms); let score = this._getItemSearchScore(itemId, terms);
if (score > 0) if (score > 0)
matchScores[itemId] = score; matchScores[itemId] = score;
} }
} else { } else {
for (let itemId in this._allItems) { for (let itemId in this._displayedItems) {
let score = this._getItemSearchScore(itemId, terms); let score = this._getItemSearchScore(itemId, terms);
if (score > 0) if (score > 0)
matchScores[itemId] = score; matchScores[itemId] = score;
@ -693,36 +756,6 @@ GenericDisplay.prototype = {
return matchScores; return matchScores;
}, },
// Applies the search string to the list of items to find matches,
// and displays the matching items.
_doSearchFilter: function(isSubSearch) {
let matchedItemsForSearch;
if (this._filterActive()) {
matchedItemsForSearch = this._getSearchMatchedItems(isSubSearch);
} else {
matchedItemsForSearch = {};
for (let itemId in this._allItems) {
matchedItemsForSearch[itemId] = 1;
}
}
this._matchedItems = [];
for (itemId in matchedItemsForSearch) {
this._matchedItems.push(itemId);
}
this._matchedItems.sort(Lang.bind(this, function (a, b) {
let countA = matchedItemsForSearch[a];
let countB = matchedItemsForSearch[b];
if (countA > countB)
return -1;
else if (countA < countB)
return 1;
else
return this._compareItems(a, b);
}));
},
/* /*
* Updates the display control to reflect the matched items set and the page selected. * Updates the display control to reflect the matched items set and the page selected.
* *