We ran into a massive performance wall causing timeouts and otherwise long page loads. Our site has thousands of nodes in groups. When loading all user groups using og_subgroup_user_groups() in places such as the entityreference selection plugin and the "user groups and subgroups" view handler, the returned array of GIDs was massive. The reason is that they aren't GIDs at all, but rather all inherited child entity IDs the user has access to via groups.
Our group structure is two level. About 30 main level groups and each have 10 to 40 subgroups.
I debugged og_subgroups_children_load_multiple() which does most of the work for og_subgroup_user_groups(). I'm not sure if og_subgroups_children_load_multiple() even promises to only return groups or if it's supposed to return all inherited child entities, but it doesn't make sense for it to return all child entities to og_subgroup_user_groups().
In detail: og_subgroups_children_load_multiple() calls og_subgroups_get_associated_entities() and that's the main problem. It makes sense if we're talking about returning all child entities, but in other use cases such as when called from og_subgroup_user_groups(), we should only get groups. I mended this locally by writing another function called og_subgroups_get_associated_groups(). When calling that instead, og_subgroups_children_load_multiple() becomes very sensible and we are experiencing okay performance.
og_subgroups_get_associated_groups() looks like this:
/**
* Get entities serving as groups that are associated with a group.
*
* @param $entity_type
* The entity type.
* @param $entity
* The entity object or entity ID.
*
* @return
* An array with the entities' entity type as the key, and array - keyed by
* the OG membership ID and the Entity ID as the value. If nothing found,
* then an empty array.
*/
function og_subgroups_get_associated_groups($entity_type, $entity, $fields = array()) {
$cache = &drupal_static(__FUNCTION__, array());
if (is_object($entity)) {
// Get the entity ID.
list($id) = entity_extract_ids($entity_type, $entity);
}
else {
$id = is_numeric($entity) ? $entity : 0;
}
if (isset($cache[$entity_type][$id])) {
// Return cached values.
return $cache[$entity_type][$id];
}
// Get all group bundles of this entity type in an array.
$group_all_bundles = og_get_all_group_bundle();
$group_bundles = isset($group_all_bundles[$entity_type]) ? array_keys($group_all_bundles[$entity_type]) : array();
// Gather scheme information from the entity to be used in the query.
$entity_info = entity_get_info($entity_type);
$entity_table = $entity_info['base table'];
$entity_key_id = $entity_info['entity keys']['id'];
$entity_key_bundle = $entity_info['entity keys']['bundle'];
$cache[$entity_type][$id] = array();
$query = db_select('og_membership');
$query->join($entity_table, 'entity', "etid = entity.{$entity_key_id}");
$query->condition('entity_type', 'user', '!=')
->condition('group_type', $entity_type, '=')
->condition('gid', $id, '=')
->condition("entity.{$entity_key_bundle}", $group_bundles, 'IN');
if ($fields || ($fields !== FALSE && $fields = variable_get('og_subgroups_default_fields_' . $entity_type, array()))) {
$query->condition('field_name', $fields, 'IN');
}
$query->fields('og_membership', array('entity_type', 'id', 'etid', 'gid'));
$query->fields('entity', array($entity_key_bundle));
$result = $query->execute();
if ($result) {
// Get the group ID from the group membership.
foreach ($result as $og_membership) {
$cache[$entity_type][$id][$og_membership->entity_type][$og_membership->id] = $og_membership->etid;
}
}
return $cache[$entity_type][$id];
}Should we change og_subgroups_children_load_multiple() to accept a parameter such as $groups_only and call the right associated content helper function depending on its value, or can we outright change what og_subgroups_children_load_multiple() returns? I think it's too late to do the latter since some custom implementations - if not this module itself - may depend on all child entities rather than only child groups being returned. However, og_subgroup_user_groups() should definitely only return GIDs and not (possibly, for the admin at least) thousands of entity IDs.
| Comment | File | Size | Author |
|---|---|---|---|
| #10 | og_subgroups-children-load-performance-2593417-10-7.diff | 5.09 KB | oetseli |
| #8 | og_subgroups-children-load-performance-2593417-8-7.diff | 13.16 KB | oetseli |
| #2 | og_subgroups-children-load-performance-2593417-1-7.patch | 4.09 KB | oetseli |
Comments
Comment #2
oetseli commentedHere's a patch we're using. It completely changes og_subgroups_childern_load_multiple() to only return groups. We can do better after a discussion, but this one solved the issue for our project. This patch also fixes the groups and subgroups view handler which didn't do anything if there were no subgroups returned. The reason it's included is, because the handler accidentally worked with the old model that returned a bunch of entity IDs, but not with this new one.
Comment #3
mpotter commentedI think we should probably change og_subgroups_get_associated_entities() itself to take an argument for $groups_only = FALSE. I've done a bit of searching and I can't find anything that uses this function that isn't intending it to return just groups. I think this was an oversight rather than a design decision, although maybe one of the original authors could weight in here.
So far, everywhere I see og_subgroups_get_associated_entities() and og_subgroups_children_load_multiple() being called it is expecting it to be just group entities returned and not all entities. So I'd even be happy to make the default of $groups_only TRUE if you wanted.
Don't think this is "critical", but it is certainly very major and I've love to get this one into Open Atrium.
Comment #4
mpotter commentedProbably need a check somewhere for $group_bundles being empty so you don't get a PDO exception.
Don't think you actually need to add the field since you don't need it in the return results.
Comment #5
oetseli commentedYes we probably should modify that original function instead. My solution has redundant duplicate code, but it's mainly there, because I didn't want to refactor the whole lot. I also suspected that this is mainly an oversight.
Still, we could do it so that $groups_only is FALSE by default and within this module we always call the function with $groups_only as TRUE. This would require every other dependant custom (or contrib, are there any?) module to update in case they want to get groups only. On the other hand we wouldn't break anything in the process. Also it makes sense to have $groups_only as FALSE by default as the function is called get_associated_entities() which in my mind promises to return everything.
Do you think we should go the $groups_only = FALSE way? I can create another patch for actual review taking your good notes into account. The entity ID field was accidentally left in the query after debugging :-).
Comment #6
tcmug commentededit: +1 to my beloved colleague :)
The name of the function is "og_subgroups_get_associated_entities", so the default state should return all entities related to the given group, however since its such a heavy operation we have to think about if its really needed; do we ever need all the content from the given group and the groups beneath it no matter how related it is in the context we're doing it in? I bet the answer is no.
Also, the parameter should be something like $groups_only = FALSE to be more in line with the function name and its default state.
Comment #7
mpotter commentedI'm fine with making the $groups_only = FALSE default and then using TRUE in all of the calls. As you say, this is the safest approach even though I doubt anybody else is using this function. And I agree that if we made the default TRUE then the name of the function should probably be different.
So go ahead and reroll the patch for this. I'm hoping to do a beta3 release later today or tomorrow and would love to get this in.
Comment #8
oetseli commentedHere's another one for review. I made sure to change all the CIDs where this parameter makes a difference. I also added another level to the $cache array in og_subgroups_get_associated_entities(). The function returns early if $groups_only and entity type has no group bundles.
Comment #9
mpotter commentedWell, this is going to take more testing now. Not something I can quickly roll into a release today.
I was thinking of just adding $only_groups to just the og_subgroups_get_associated_entities() function since the og_subgroups_children_load_multiple() function already has the comment:
/**
* Get the inheriented groups + current group.
*/
that implies it only returns groups. In fact, it might even be considered a bug that og_subgroups_children_load_multiple() returns anything other than groups. So I'm not sure I'm happy with adding $only_groups to so many other functions.
Comment #10
oetseli commentedAh that's true. It does only promise groups so maybe another one without the $groups_only in og_subgroups_children_load_multiple() and og_subgroups_children_load(). That would also mean we can drop CID changes.
Here's a much simpler patch. Although I see you already did a release, but here goes anyway.
Comment #11
drubage commentedWas this patch ever added to the module? We are seeing the same performance issues. We have a content type called "market" that is a group and then we post "properties" into the "market". When every page loads there is a HUGE node_access query with every property node ID in it. I am not sure what that would relevant to be loaded on all the pages especially since the properties are not groups only the markets are. There are thousands of properties so for users assigned to more than 1 market the site is completely unusable. Any ideas on how to get past this performance bottleneck? Thanks!
-Drew
Comment #13
mpotter commentedDid some testing and I don't see any side-effects with the patch in #10 and it's nice and clean. So committed to 8a42077.