Merge pull request #173 from Tiktack/threading-performance
Improve performance of API Threading strategy.
This commit is contained in:
@@ -7,6 +7,11 @@ namespace Wino.Core.Domain.Interfaces
|
|||||||
{
|
{
|
||||||
public interface IThreadingStrategy
|
public interface IThreadingStrategy
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attach thread mails to the list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">Original mails.</param>
|
||||||
|
/// <returns>Original mails with thread mails.</returns>
|
||||||
Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items);
|
Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items);
|
||||||
bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem);
|
bool ShouldThreadWithItem(IMailItem originalItem, IMailItem targetItem);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,87 +26,55 @@ namespace Wino.Core.Integration.Threading
|
|||||||
return originalItem.ThreadId != null && originalItem.ThreadId == targetItem.ThreadId;
|
return originalItem.ThreadId != null && originalItem.ThreadId == targetItem.ThreadId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///<inheritdoc/>
|
||||||
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items)
|
public async Task<List<IMailItem>> ThreadItemsAsync(List<MailCopy> items)
|
||||||
{
|
{
|
||||||
var accountId = items.First().AssignedAccount.Id;
|
var assignedAccount = items[0].AssignedAccount;
|
||||||
|
|
||||||
var threads = new List<ThreadMailItem>();
|
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent);
|
||||||
var assignedAccount = items.First().AssignedAccount;
|
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Draft);
|
||||||
|
|
||||||
// TODO: Can be optimized by moving to the caller.
|
|
||||||
var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, Domain.Enums.SpecialFolderType.Sent);
|
|
||||||
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(accountId, Domain.Enums.SpecialFolderType.Draft);
|
|
||||||
|
|
||||||
if (sentFolder == null || draftFolder == null) return default;
|
if (sentFolder == null || draftFolder == null) return default;
|
||||||
|
|
||||||
// Child -> Parent approach.
|
// True: Non threaded items.
|
||||||
|
// False: Potentially threaded items.
|
||||||
|
var nonThreadedOrThreadedMails = items
|
||||||
|
.Distinct()
|
||||||
|
.GroupBy(x => string.IsNullOrEmpty(x.ThreadId))
|
||||||
|
.ToDictionary(x => x.Key, x => x);
|
||||||
|
|
||||||
var potentialThreadItems = items.Distinct().Where(a => !string.IsNullOrEmpty(a.ThreadId));
|
_ = nonThreadedOrThreadedMails.TryGetValue(true, out var nonThreadedMails);
|
||||||
|
var isThreadedItems = nonThreadedOrThreadedMails.TryGetValue(false, out var potentiallyThreadedMails);
|
||||||
|
|
||||||
var mailLookupTable = new Dictionary<string, bool>();
|
List<IMailItem> resultList = nonThreadedMails is null ? [] : [.. nonThreadedMails];
|
||||||
|
|
||||||
// Fill up the mail lookup table to prevent double thread creation.
|
if (isThreadedItems)
|
||||||
foreach (var mail in items)
|
|
||||||
if (!mailLookupTable.ContainsKey(mail.Id))
|
|
||||||
mailLookupTable.Add(mail.Id, false);
|
|
||||||
|
|
||||||
foreach (var potentialItem in potentialThreadItems)
|
|
||||||
{
|
{
|
||||||
if (mailLookupTable[potentialItem.Id])
|
var threadItems = (await GetThreadItemsAsync(potentiallyThreadedMails.Select(x => (x.ThreadId, x.AssignedFolder)).ToList(), assignedAccount.Id, sentFolder.Id, draftFolder.Id))
|
||||||
|
.GroupBy(x => x.ThreadId);
|
||||||
|
|
||||||
|
foreach (var threadItem in threadItems)
|
||||||
|
{
|
||||||
|
if (threadItem.Count() == 1)
|
||||||
|
{
|
||||||
|
resultList.Add(threadItem.First());
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
mailLookupTable[potentialItem.Id] = true;
|
var thread = new ThreadMailItem();
|
||||||
|
foreach (var childThreadItem in threadItem)
|
||||||
var allThreadItems = await GetThreadItemsAsync(potentialItem.ThreadId, accountId, potentialItem.AssignedFolder, sentFolder.Id, draftFolder.Id);
|
|
||||||
|
|
||||||
if (allThreadItems.Count == 1)
|
|
||||||
{
|
{
|
||||||
// It's a single item.
|
thread.AddThreadItem(childThreadItem);
|
||||||
// Mark as not-processed as thread.
|
|
||||||
|
|
||||||
mailLookupTable[potentialItem.Id] = false;
|
|
||||||
}
|
}
|
||||||
else
|
resultList.Add(thread);
|
||||||
{
|
|
||||||
// Thread item. Mark all items as true in dict.
|
|
||||||
var threadItem = new ThreadMailItem();
|
|
||||||
|
|
||||||
foreach (var childThreadItem in allThreadItems)
|
|
||||||
{
|
|
||||||
if (mailLookupTable.ContainsKey(childThreadItem.Id))
|
|
||||||
mailLookupTable[childThreadItem.Id] = true;
|
|
||||||
|
|
||||||
childThreadItem.AssignedAccount = assignedAccount;
|
|
||||||
childThreadItem.AssignedFolder = await _folderService.GetFolderAsync(childThreadItem.FolderId);
|
|
||||||
|
|
||||||
threadItem.AddThreadItem(childThreadItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiple mail copy ids from different folders are thing for Gmail.
|
|
||||||
if (threadItem.ThreadItems.Count == 1)
|
|
||||||
mailLookupTable[potentialItem.Id] = false;
|
|
||||||
else
|
|
||||||
threads.Add(threadItem);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this points all mails in the list belong to single items.
|
return resultList;
|
||||||
// Merge with threads.
|
|
||||||
// Last sorting will be done later on in MailService.
|
|
||||||
|
|
||||||
// Remove single mails that are included in thread.
|
|
||||||
items.RemoveAll(a => mailLookupTable.ContainsKey(a.Id) && mailLookupTable[a.Id]);
|
|
||||||
|
|
||||||
var finalList = new List<IMailItem>(items);
|
|
||||||
|
|
||||||
finalList.AddRange(threads);
|
|
||||||
|
|
||||||
return finalList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<MailCopy>> GetThreadItemsAsync(string threadId,
|
private async Task<List<MailCopy>> GetThreadItemsAsync(List<(string threadId, MailItemFolder threadingFolder)> potentialThread,
|
||||||
Guid accountId,
|
Guid accountId,
|
||||||
MailItemFolder threadingFolder,
|
|
||||||
Guid sentFolderId,
|
Guid sentFolderId,
|
||||||
Guid draftFolderId)
|
Guid draftFolderId)
|
||||||
{
|
{
|
||||||
@@ -118,24 +86,20 @@ namespace Wino.Core.Integration.Threading
|
|||||||
|
|
||||||
// TODO: Convert to SQLKata query.
|
// TODO: Convert to SQLKata query.
|
||||||
|
|
||||||
string query = string.Empty;
|
var query = @$"SELECT DISTINCT MC.* FROM MailCopy MC
|
||||||
|
|
||||||
if (threadingFolder.SpecialFolderType == SpecialFolderType.Draft || threadingFolder.SpecialFolderType == SpecialFolderType.Sent)
|
|
||||||
{
|
|
||||||
query = @$"SELECT DISTINCT MC.* FROM MailCopy MC
|
|
||||||
INNER JOIN MailItemFolder MF on MF.Id = MC.FolderId
|
INNER JOIN MailItemFolder MF on MF.Id = MC.FolderId
|
||||||
WHERE MF.MailAccountId == '{accountId}' AND MC.ThreadId = '{threadId}'";
|
WHERE MF.MailAccountId == '{accountId}' AND
|
||||||
}
|
({string.Join(" OR ", potentialThread.Select(x => ConditionForItem(x, sentFolderId, draftFolderId)))})";
|
||||||
else
|
|
||||||
{
|
|
||||||
query = @$"SELECT DISTINCT MC.* FROM MailCopy MC
|
|
||||||
INNER JOIN MailItemFolder MF on MF.Id = MC.FolderId
|
|
||||||
WHERE MF.MailAccountId == '{accountId}' AND MC.FolderId IN ('{threadingFolder.Id}','{sentFolderId}','{draftFolderId}')
|
|
||||||
AND MC.ThreadId = '{threadId}'";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return await _databaseService.Connection.QueryAsync<MailCopy>(query);
|
return await _databaseService.Connection.QueryAsync<MailCopy>(query);
|
||||||
|
|
||||||
|
static string ConditionForItem((string threadId, MailItemFolder threadingFolder) potentialThread, Guid sentFolderId, Guid draftFolderId)
|
||||||
|
{
|
||||||
|
if (potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Draft || potentialThread.threadingFolder.SpecialFolderType == SpecialFolderType.Sent)
|
||||||
|
return $"(MC.ThreadId = '{potentialThread.threadId}')";
|
||||||
|
|
||||||
|
return $"(MC.ThreadId = '{potentialThread.threadId}' AND MC.FolderId IN ('{potentialThread.threadingFolder.Id}','{sentFolderId}','{draftFolderId}'))";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Kiota.Abstractions.Extensions;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MimeKit.Text;
|
using MimeKit.Text;
|
||||||
using MoreLinq;
|
using MoreLinq;
|
||||||
@@ -186,46 +187,35 @@ namespace Wino.Core.Services
|
|||||||
|
|
||||||
var mails = await Connection.QueryAsync<MailCopy>(query);
|
var mails = await Connection.QueryAsync<MailCopy>(query);
|
||||||
|
|
||||||
// Fill in assigned account and folder for each mail.
|
Dictionary<Guid, MailItemFolder> folderCache = [];
|
||||||
// To speed things up a bit, we'll load account and assigned folder in groups
|
Dictionary<Guid, MailAccount> accountCache = [];
|
||||||
// to reduce the query time.
|
|
||||||
|
|
||||||
var groupedByFolders = mails.GroupBy(a => a.FolderId);
|
// Populate Folder Assignment for each single mail, to be able later group by "MailAccountId".
|
||||||
|
// This is needed to execute threading strategy by account type.
|
||||||
foreach (var group in groupedByFolders)
|
// Avoid DBs calls as possible, storing info in a dictionary.
|
||||||
|
foreach (var mail in mails)
|
||||||
{
|
{
|
||||||
MailItemFolder folderAssignment = null;
|
await LoadAssignedPropertiesWithCacheAsync(mail, folderCache, accountCache).ConfigureAwait(false);
|
||||||
MailAccount accountAssignment = null;
|
|
||||||
|
|
||||||
folderAssignment = await _folderService.GetFolderAsync(group.Key).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (folderAssignment != null)
|
|
||||||
{
|
|
||||||
accountAssignment = await _accountService.GetAccountAsync(folderAssignment.MailAccountId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
group.ForEach(a =>
|
|
||||||
{
|
|
||||||
a.AssignedFolder = folderAssignment;
|
|
||||||
a.AssignedAccount = accountAssignment;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove items that has no assigned account or folder.
|
// Remove items that has no assigned account or folder.
|
||||||
mails.RemoveAll(a => a.AssignedAccount == null || a.AssignedFolder == null);
|
mails.RemoveAll(a => a.AssignedAccount == null || a.AssignedFolder == null);
|
||||||
|
|
||||||
// Each account items must be threaded separately.
|
if (!options.CreateThreads)
|
||||||
|
|
||||||
if (options.CreateThreads)
|
|
||||||
{
|
{
|
||||||
|
// Threading is disabled. Just return everything as it is.
|
||||||
|
mails.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
|
||||||
|
|
||||||
|
return new List<IMailItem>(mails);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate threaded items.
|
||||||
|
|
||||||
var threadedItems = new List<IMailItem>();
|
var threadedItems = new List<IMailItem>();
|
||||||
|
|
||||||
var groupedByAccounts = mails.GroupBy(a => a.AssignedAccount.Id);
|
// Each account items must be threaded separately.
|
||||||
|
foreach (var group in mails.GroupBy(a => a.AssignedAccount.Id))
|
||||||
foreach (var group in groupedByAccounts)
|
|
||||||
{
|
{
|
||||||
if (!group.Any()) continue;
|
|
||||||
|
|
||||||
var accountId = group.Key;
|
var accountId = group.Key;
|
||||||
var groupAccount = mails.First(a => a.AssignedAccount.Id == accountId).AssignedAccount;
|
var groupAccount = mails.First(a => a.AssignedAccount.Id == accountId).AssignedAccount;
|
||||||
|
|
||||||
@@ -233,7 +223,14 @@ namespace Wino.Core.Services
|
|||||||
|
|
||||||
// Only thread items from Draft and Sent folders must present here.
|
// Only thread items from Draft and Sent folders must present here.
|
||||||
// Otherwise this strategy will fetch the items that are in Deleted folder as well.
|
// Otherwise this strategy will fetch the items that are in Deleted folder as well.
|
||||||
var accountThreadedItems = await threadingStrategy.ThreadItemsAsync(group.ToList());
|
var accountThreadedItems = await threadingStrategy.ThreadItemsAsync([.. group]);
|
||||||
|
|
||||||
|
// Populate threaded items with folder and account assignments.
|
||||||
|
// Almost everything already should be in cache from initial population.
|
||||||
|
foreach (var mail in accountThreadedItems)
|
||||||
|
{
|
||||||
|
await LoadAssignedPropertiesWithCacheAsync(mail, folderCache, accountCache).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (accountThreadedItems != null)
|
if (accountThreadedItems != null)
|
||||||
{
|
{
|
||||||
@@ -244,14 +241,42 @@ namespace Wino.Core.Services
|
|||||||
threadedItems.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
|
threadedItems.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
|
||||||
|
|
||||||
return threadedItems;
|
return threadedItems;
|
||||||
}
|
|
||||||
else
|
// Recursive function to populate folder and account assignments for each mail item.
|
||||||
|
async Task LoadAssignedPropertiesWithCacheAsync(IMailItem mail, Dictionary<Guid, MailItemFolder> folderCache, Dictionary<Guid, MailAccount> accountCache)
|
||||||
{
|
{
|
||||||
// Threading is disabled. Just return everything as it is.
|
if (mail is ThreadMailItem threadMailItem)
|
||||||
|
{
|
||||||
|
foreach (var childMail in threadMailItem.ThreadItems)
|
||||||
|
{
|
||||||
|
await LoadAssignedPropertiesWithCacheAsync(childMail, folderCache, accountCache).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mails.Sort(options.SortingOptionType == SortingOptionType.ReceiveDate ? new DateComparer() : new NameComparer());
|
if (mail is MailCopy mailCopy)
|
||||||
|
{
|
||||||
|
MailAccount accountAssignment = null;
|
||||||
|
|
||||||
return new List<IMailItem>(mails);
|
var isFolderCached = folderCache.TryGetValue(mailCopy.FolderId, out MailItemFolder folderAssignment);
|
||||||
|
accountAssignment = null;
|
||||||
|
if (!isFolderCached)
|
||||||
|
{
|
||||||
|
folderAssignment = await _folderService.GetFolderAsync(mailCopy.FolderId).ConfigureAwait(false);
|
||||||
|
_ = folderCache.TryAdd(mailCopy.FolderId, folderAssignment);
|
||||||
|
}
|
||||||
|
if (folderAssignment != null)
|
||||||
|
{
|
||||||
|
var isAccountCached = accountCache.TryGetValue(folderAssignment.MailAccountId, out accountAssignment);
|
||||||
|
if (!isAccountCached)
|
||||||
|
{
|
||||||
|
accountAssignment = await _accountService.GetAccountAsync(folderAssignment.MailAccountId).ConfigureAwait(false);
|
||||||
|
_ = accountCache.TryAdd(folderAssignment.MailAccountId, accountAssignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mailCopy.AssignedFolder = folderAssignment;
|
||||||
|
mailCopy.AssignedAccount = accountAssignment;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user