Skip to content

Commit f9ef112

Browse files
authored
perf(worker): rewrite voice-event sweep as bisect, drop to O(n log n) (#69)
1 parent 4494c4a commit f9ef112

1 file changed

Lines changed: 50 additions & 30 deletions

File tree

src/tasks.py

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -563,47 +563,67 @@ def read_analytics_file(package_status_id, package_id, link, session):
563563

564564
'''
565565
Process voice channel logs to get a list of "events"
566+
567+
The original implementation was O(n*m + n*k) per channel: a linear scan of
568+
all leaves to find the next leave per join, plus a linear scan of every
569+
accepted event so far to dedupe overlapping joins. For heavy users with
570+
thousands of voice events this blew through the 15-min Lambda cap.
571+
572+
Now: bisect on sorted-leave timestamps for the next-leave lookup, and a
573+
parallel sorted list of accepted events keyed by started_date for the
574+
overlap check. O((n+m) log m + n log k).
566575
'''
567-
# Group voice channel logs by channel_id
576+
import bisect
577+
568578
logs_by_channel = {k: list(v) for k, v in groupby(sorted(voice_channel_logs, key=lambda x: x['channel_id']), key=lambda x: x['channel_id'])}
569579
voice_channel_logs_duration = []
580+
581+
# Parallel structures for O(log k) overlap queries against the global
582+
# event list. Voice events on Discord are non-overlapping per user, so
583+
# only the rightmost candidate (largest started_date <= t) needs checking.
584+
accepted_starts = []
585+
accepted_events = []
586+
587+
def _is_in_existing_event(t):
588+
i = bisect.bisect_right(accepted_starts, t)
589+
if i == 0:
590+
return False
591+
e = accepted_events[i - 1]
592+
return e['ended_date'] >= t
593+
570594
for channel_id, logs in logs_by_channel.items():
571-
# Separate join and leave events
572595
joins = [x for x in logs if x['event_type'] == 'join_voice_channel']
573596
leaves = [x for x in logs if x['event_type'] == 'leave_voice_channel']
574597

575-
# Sort events by timestamp
576598
sorted_joins = sorted(joins, key=lambda x: x['timestamp'])
577599
sorted_leaves = sorted(leaves, key=lambda x: x['timestamp'])
578-
600+
leave_timestamps = [l['timestamp'] for l in sorted_leaves]
601+
579602
for join in sorted_joins:
580-
# Find the next leave event that happened after this join
581-
next_leave = next((leave for leave in sorted_leaves if leave['timestamp'] >= join['timestamp']), None)
582-
583-
# Calculate duration
584-
duration = next_leave['timestamp'] - join['timestamp'] if next_leave else 0
585-
603+
j_ts = join['timestamp']
604+
605+
idx = bisect.bisect_left(leave_timestamps, j_ts)
606+
if idx >= len(sorted_leaves):
607+
continue
608+
next_leave = sorted_leaves[idx]
609+
duration = next_leave['timestamp'] - j_ts
610+
586611
if duration > 24 * 60 * 60:
587-
pass
588-
elif not next_leave:
589-
pass
590-
else:
591-
join_is_included_in_duration = any(
592-
join['timestamp'] >= e['started_date'] # this join happened after the start of another event
593-
and join['timestamp'] <= e['ended_date'] # this same event ended after this join
594-
for e in voice_channel_logs_duration)
595-
596-
if join_is_included_in_duration:
597-
#print(f'Join is included in duration: {join}')
598-
pass
599-
else:
600-
voice_channel_logs_duration.append({
601-
'channel_id': channel_id,
602-
'guild_id': join['guild_id'] if 'guild_id' in join else None,
603-
'duration_mins': duration // 60,
604-
'started_date': join['timestamp'],
605-
'ended_date': next_leave['timestamp'] if next_leave else None
606-
})
612+
continue
613+
if _is_in_existing_event(j_ts):
614+
continue
615+
616+
event = {
617+
'channel_id': channel_id,
618+
'guild_id': join['guild_id'] if 'guild_id' in join else None,
619+
'duration_mins': duration // 60,
620+
'started_date': j_ts,
621+
'ended_date': next_leave['timestamp'],
622+
}
623+
voice_channel_logs_duration.append(event)
624+
ins = bisect.bisect_right(accepted_starts, j_ts)
625+
accepted_starts.insert(ins, j_ts)
626+
accepted_events.insert(ins, event)
607627

608628
session_logs_duration = []
609629

0 commit comments

Comments
 (0)