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:
Noko 2019-10-05 15:08:27 -05:00
parent 0ec522ca0a
commit 845ec90c3e

View File

@ -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
// we create the entry objects var entries =
var outList = new List<SwitchListEntry>(); from item in switchMembers
group item by item.Timestamp into g
// loop through every switch that *occurred* in-range and add it to the list select new SwitchListEntry
// 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;
foreach (var switchInRange in switchesInRange)
{ {
// 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) TimespanStart = g.Key,
var switchStartClamped = switchInRange.Timestamp; Members = g.Select(x => memberObjects[x.Member]).ToList()
if (switchStartClamped < periodStart) switchStartClamped = periodStart; };
// Loop through every switch that overlaps the range and add it to the output list
// end time is the *FOLLOWING* switch's timestamp - we cheat by working backwards from the range end, so no dates need to be compared
var endTime = periodEnd;
var outList = new List<SwitchListEntry>();
foreach (var e in entries)
{
// 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 = e.TimespanStart < 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;