@@ -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