Avoid sorting in UserPresence query.

We were using `.order_by('user_profile_id', '-timestamp') in our
UserPresence query in get_status_dicts_for_query.

We don't need a full sort to produce the dictionary of statuses.
In fact the whole operation in Python is still O(N):

    - divvy rows up to be per-user in an O(N) pass
    - find max row for the 'aggregated' entry in an O(n) pass
      per user

The one minor annoyance of this fix is that datetime_to_timestamp
is lossy, so if you naively call to_presence_dict before finding
the "max" row, you get test flakes if rows are created during the
same second.  I decided to avoid calling to_presence_dict so there
are fewer moving parts, but there's still the ugly step of having
to remove the "dt" field from the final results.
This commit is contained in:
Steve Howell 2017-09-08 06:22:14 -07:00 committed by showell
parent 19e134698e
commit c19b3aec0c

View File

@ -1617,7 +1617,7 @@ class UserPresence(models.Model):
@staticmethod
def get_status_dict_by_user(user_profile):
# type: (UserProfile) -> DefaultDict[Any, Dict[Any, Any]]
# type: (UserProfile) -> Dict[Text, Dict[Any, Any]]
query = UserPresence.objects.filter(user_profile=user_profile).values(
'client__name',
'status',
@ -1643,7 +1643,7 @@ class UserPresence(models.Model):
@staticmethod
def get_status_dict_by_realm(realm_id):
# type: (int) -> DefaultDict[Any, Dict[Any, Any]]
# type: (int) -> Dict[Text, Dict[Any, Any]]
query = UserPresence.objects.filter(
user_profile__realm_id=realm_id,
user_profile__is_active=True,
@ -1672,26 +1672,53 @@ class UserPresence(models.Model):
@staticmethod
def get_status_dicts_for_query(query, mobile_user_ids):
# type: (QuerySet, List[int]) -> DefaultDict[Any, Dict[Any, Any]]
user_statuses = defaultdict(dict) # type: DefaultDict[Any, Dict[Any, Any]]
# Order of query is important to get a latest status as aggregated status.
for row in query.order_by("user_profile__id", "-timestamp"):
info = UserPresence.to_presence_dict(
row['client__name'],
row['status'],
row['timestamp'],
push_enabled=row['user_profile__enable_offline_push_notifications'],
has_push_devices=row['user_profile__id'] in mobile_user_ids,
is_mirror_dummy=row['user_profile__is_mirror_dummy'],
# type: (QuerySet, List[int]) -> Dict[Text, Dict[Any, Any]]
info_row_dct = defaultdict(list) # type: DefaultDict[Text, List[Dict[str, Any]]]
for row in query:
email = row['user_profile__email']
client_name = row['client__name']
status = UserPresence.status_to_string(row['status'])
dt = row['timestamp']
timestamp = datetime_to_timestamp(dt)
push_enabled = row['user_profile__enable_offline_push_notifications']
has_push_devices = row['user_profile__id'] in mobile_user_ids
pushable = (push_enabled and has_push_devices)
info = dict(
client=client_name,
status=status,
dt=dt,
timestamp=timestamp,
pushable=pushable,
)
if not user_statuses.get(row['user_profile__email']):
# Applying the latest status as aggregated status for user.
user_statuses[row['user_profile__email']]['aggregated'] = {
'status': info['status'],
'timestamp': info['timestamp'],
'client': info['client']
}
user_statuses[row['user_profile__email']][row['client__name']] = info
info_row_dct[email].append(info)
user_statuses = dict() # type: Dict[str, Dict[str, Any]]
for email, info_rows in info_row_dct.items():
# Note that datetime values have sub-second granularity, which is
# mostly important for avoiding test flakes, but it's also technically
# more precise for real users.
by_time = lambda row: row['dt']
most_recent_info = max(info_rows, key=by_time)
# We don't send datetime values to the client.
for r in info_rows:
del r['dt']
client_dict = {info['client']: info for info in info_rows}
user_statuses[email] = client_dict
# The word "aggegrated" here is possibly misleading.
# It's really just the most recent client's info.
user_statuses[email]['aggregated'] = dict(
client=most_recent_info['client'],
status=most_recent_info['status'],
timestamp=most_recent_info['timestamp'],
)
return user_statuses
@staticmethod