Improve frontpercent performance
Refactored GetTruncatedSwitchList to:
- Only fetch switches in the requested range
- Fetch switch members in bulk rather than one switch at a time
This uses a new GetSwitchMembersList method that requires the following indexes:
CREATE INDEX ix_switches_system
ON public.switches USING btree
(system ASC NULLS LAST)
INCLUDE("timestamp")
TABLESPACE pg_default;
CREATE INDEX ix_switch_members_switch
ON public.switch_members USING btree
(switch ASC NULLS LAST)
INCLUDE(member)
TABLESPACE pg_default;
			
			
This commit is contained in:
		@@ -302,6 +302,48 @@ namespace PluralKit {
 | 
				
			|||||||
                return await conn.QueryAsync<PKSwitch>("select * from switches where system = @System order by timestamp desc limit @Count", new {System = system.Id, Count = count});
 | 
					                return await conn.QueryAsync<PKSwitch>("select * from switches where system = @System order by timestamp desc limit @Count", new {System = system.Id, Count = count});
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public struct SwitchMembersListEntry
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            public int Member;
 | 
				
			||||||
 | 
					            public Instant Timestamp;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public async Task<IEnumerable<SwitchMembersListEntry>> GetSwitchMembersList(PKSystem system, Instant start, Instant end)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // Wrap multiple commands in a single transaction for performance
 | 
				
			||||||
 | 
					            using (var conn = await _conn.Obtain())
 | 
				
			||||||
 | 
					            using (var tx = conn.BeginTransaction())
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                // Find the time of the last switch outside the range as it overlaps the range
 | 
				
			||||||
 | 
					                // If no prior switch exists, the lower bound of the range remains the start time
 | 
				
			||||||
 | 
					                var lastSwitch = await conn.QuerySingleOrDefaultAsync<Instant>(
 | 
				
			||||||
 | 
					                    @"SELECT COALESCE(MAX(timestamp), @Start)
 | 
				
			||||||
 | 
					                        FROM switches
 | 
				
			||||||
 | 
					                        WHERE switches.system = @System
 | 
				
			||||||
 | 
					                        AND switches.timestamp < @Start",
 | 
				
			||||||
 | 
					                    new { System = system.Id, Start = start });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Then collect the time and members of all switches that overlap the range
 | 
				
			||||||
 | 
					                var switchMembersEntries = await conn.QueryAsync<SwitchMembersListEntry>(
 | 
				
			||||||
 | 
					                    @"SELECT switch_members.member, switches.timestamp
 | 
				
			||||||
 | 
					                        FROM switches
 | 
				
			||||||
 | 
					                        JOIN switch_members
 | 
				
			||||||
 | 
					                        ON switches.id = switch_members.switch
 | 
				
			||||||
 | 
					                        WHERE switches.system = @System
 | 
				
			||||||
 | 
					                        AND (
 | 
				
			||||||
 | 
						                        switches.timestamp >= @Start
 | 
				
			||||||
 | 
						                        OR switches.timestamp = @LastSwitch
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        AND switches.timestamp < @End
 | 
				
			||||||
 | 
					                        ORDER BY switches.timestamp DESC",
 | 
				
			||||||
 | 
					                    new { System = system.Id, Start = start, End = end, LastSwitch = lastSwitch });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Commit and return the list
 | 
				
			||||||
 | 
					                tx.Commit();
 | 
				
			||||||
 | 
					                return switchMembersEntries;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public async Task<IEnumerable<int>> GetSwitchMemberIds(PKSwitch sw)
 | 
					        public async Task<IEnumerable<int>> GetSwitchMemberIds(PKSwitch sw)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            using (var conn = await _conn.Obtain())
 | 
					            using (var conn = await _conn.Obtain())
 | 
				
			||||||
@@ -351,14 +393,8 @@ namespace PluralKit {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        public async Task<IEnumerable<SwitchListEntry>> GetTruncatedSwitchList(PKSystem system, Instant periodStart, Instant periodEnd)
 | 
					        public async Task<IEnumerable<SwitchListEntry>> GetTruncatedSwitchList(PKSystem system, Instant periodStart, Instant periodEnd)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            // TODO: only fetch the necessary switches here
 | 
					            // Returns the timestamps and member IDs of switches overlapping the range, in chronological (newest first) order
 | 
				
			||||||
            // todo: this is in general not very efficient LOL
 | 
					            var switchMembers = await GetSwitchMembersList(system, periodStart, periodEnd);
 | 
				
			||||||
            // returns switches in chronological (newest first) order
 | 
					 | 
				
			||||||
            var switches = await GetSwitches(system);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            // we skip all switches that happened later than the range end, and taking all the ones that happened after the range start
 | 
					 | 
				
			||||||
            // *BUT ALSO INCLUDING* the last switch *before* the range (that partially overlaps the range period)
 | 
					 | 
				
			||||||
            var switchesInRange = switches.SkipWhile(sw => sw.Timestamp >= periodEnd).TakeWhileIncluding(sw => sw.Timestamp > periodStart).ToList();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // query DB for all members involved in any of the switches above and collect into a dictionary for future use
 | 
					            // query DB for all members involved in any of the switches above and collect into a dictionary for future use
 | 
				
			||||||
            // this makes sure the return list has the same instances of PKMember throughout, which is important for the dictionary
 | 
					            // this makes sure the return list has the same instances of PKMember throughout, which is important for the dictionary
 | 
				
			||||||
@@ -366,34 +402,43 @@ namespace PluralKit {
 | 
				
			|||||||
            Dictionary<int, PKMember> memberObjects;
 | 
					            Dictionary<int, PKMember> memberObjects;
 | 
				
			||||||
            using (var conn = await _conn.Obtain())
 | 
					            using (var conn = await _conn.Obtain())
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                memberObjects = (await conn.QueryAsync<PKMember>(
 | 
					                memberObjects = (
 | 
				
			||||||
                        "select distinct members.* from members, switch_members where switch_members.switch = any(@Switches) and switch_members.member = members.id", // lol postgres specific `= any()` syntax
 | 
					                    await conn.QueryAsync<PKMember>(
 | 
				
			||||||
                        new {Switches = switchesInRange.Select(sw => sw.Id).ToList()}))
 | 
					                        "select * from members where id = any(@Switches)", // lol postgres specific `= any()` syntax
 | 
				
			||||||
                    .ToDictionary(m => m.Id);
 | 
					                        new { Switches = switchMembers.Select(m => m.Member).Distinct().ToList() })
 | 
				
			||||||
 | 
					                    ).ToDictionary(m => m.Id);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Initialize entries - still need to loop to determine the TimespanEnd below
 | 
				
			||||||
 | 
					            var entries =
 | 
				
			||||||
 | 
					                from item in switchMembers
 | 
				
			||||||
 | 
					                group item by item.Timestamp into g
 | 
				
			||||||
 | 
					                select new SwitchListEntry
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    TimespanStart = g.Key,
 | 
				
			||||||
 | 
					                    Members = g.Select(x => memberObjects[x.Member]).ToList()
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // we create the entry objects
 | 
					            // Loop through every switch that overlaps the range and add it to the output list
 | 
				
			||||||
            var outList = new List<SwitchListEntry>();
 | 
					            // end time is the *FOLLOWING* switch's timestamp - we cheat by working backwards from the range end, so no dates need to be compared
 | 
				
			||||||
 | 
					 | 
				
			||||||
            // loop through every switch that *occurred* in-range and add it to the list
 | 
					 | 
				
			||||||
            // end time is the switch *after*'s timestamp - we cheat and start it out at the range end so the first switch in-range "ends" there instead of the one after's start point
 | 
					 | 
				
			||||||
            var endTime = periodEnd;
 | 
					            var endTime = periodEnd;
 | 
				
			||||||
            foreach (var switchInRange in switchesInRange)
 | 
					            var outList = new List<SwitchListEntry>();
 | 
				
			||||||
 | 
					            foreach (var e in entries)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                // find the start time of the switch, but clamp it to the range (only applicable to the Last Switch Before Range we include in the TakeWhileIncluding call above)
 | 
					                // Override the start time of the switch if it's outside the range (only true for the "out of range" switch we included above)
 | 
				
			||||||
                var switchStartClamped = switchInRange.Timestamp;
 | 
					                var switchStartClamped = e.TimespanStart < periodStart
 | 
				
			||||||
                if (switchStartClamped < periodStart) switchStartClamped = periodStart;
 | 
					                    ? periodStart
 | 
				
			||||||
 | 
					                    : e.TimespanStart;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                outList.Add(new SwitchListEntry
 | 
					                outList.Add(new SwitchListEntry
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    Members = (await GetSwitchMemberIds(switchInRange)).Select(id => memberObjects[id]).ToList(),
 | 
					                    Members = e.Members,
 | 
				
			||||||
                    TimespanStart = switchStartClamped,
 | 
					                    TimespanStart = switchStartClamped,
 | 
				
			||||||
                    TimespanEnd = endTime
 | 
					                    TimespanEnd = endTime
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // next switch's end is this switch's start
 | 
					                // next switch's end is this switch's start (we're working backward in time)
 | 
				
			||||||
                endTime = switchInRange.Timestamp;
 | 
					                endTime = e.TimespanStart;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return outList;
 | 
					            return outList;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user