diff options
| author | brandon <brandon@clinger.dev> | 2026-08-07 11:59:17 -0400 |
|---|---|---|
| committer | brandon <brandon@clinger.dev> | 2026-08-07 11:59:17 -0400 |
| commit | d6da6906a4b3426f05c5bf4ffe8bab4b2bc78ce8 (patch) | |
| tree | 78683665d1792916de8683b6ac5bc13e17904d87 /Emby.Server.Implementations | |
| parent | 6c37a6ef8b4ce027e7ac2aaa827244711cf5f39c (diff) | |
Batch people lookups when building item DTOs
GetBaseItemDtos already batch fetches user data, child counts, played counts
and artists before its per item loop, but AttachPeople still ran one GetPeople
query per item. Rendering a page of items (for example a large playlist) fired
one extra query per row.
Add GetPeopleByItems to IPeopleRepository, which reads every requested item in a
single query over the people mapping table and returns full PersonInfo (role,
type and sort order) grouped by item id. GetBaseItemDtos prefetches this once
when the People field is requested and passes it into AttachPeople, which reads
from the batch instead of querying per item. The single item GetBaseItemDto path
keeps its existing per item behaviour when no batch is supplied.
Adds a DtoService test asserting people resolve from the batch and the per item
GetPeople is never called.
Diffstat (limited to 'Emby.Server.Implementations')
| -rw-r--r-- | Emby.Server.Implementations/Dto/DtoService.cs | 37 | ||||
| -rw-r--r-- | Emby.Server.Implementations/Library/LibraryManager.cs | 6 |
2 files changed, 38 insertions, 5 deletions
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 71c3b24907..da0c52df5b 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -242,6 +242,17 @@ namespace Emby.Server.Implementations.Dto artistsBatch = _libraryManager.GetArtists(artistNames.ToArray()); } + // Batch-fetch people across all items to avoid one GetPeople query per item. + IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null; + if (options.ContainsField(ItemFields.People)) + { + var peopleItemIds = accessibleItems.Where(i => i.SupportsPeople).Select(i => i.Id).ToList(); + if (peopleItemIds.Count > 0) + { + peopleBatch = _libraryManager.GetPeopleByItems(peopleItemIds); + } + } + for (int index = 0; index < accessibleItems.Count; index++) { var item = accessibleItems[index]; @@ -255,7 +266,8 @@ namespace Emby.Server.Implementations.Dto childCountBatch, playedCountBatch, artistsBatch, - resumeDataBatch?.GetValueOrDefault(item.Id)); + resumeDataBatch?.GetValueOrDefault(item.Id), + peopleBatch); if (item is LiveTvChannel tvChannel) { @@ -317,7 +329,8 @@ namespace Emby.Server.Implementations.Dto Dictionary<Guid, int>? childCountBatch = null, Dictionary<Guid, (int Played, int Total)>? playedCountBatch = null, IReadOnlyDictionary<string, MusicArtist[]>? artistsBatch = null, - VersionResumeData? resumeData = null) + VersionResumeData? resumeData = null, + IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>>? peopleBatch = null) { var dto = new BaseItemDto { @@ -331,7 +344,15 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.People)) { - AttachPeople(dto, item, user); + IReadOnlyList<PersonInfo>? prefetchedPeople = null; + if (peopleBatch is not null) + { + // The batch omits items with no people, so a miss means "no people", + // not "not fetched". Use an empty list to skip the per-item query. + prefetchedPeople = peopleBatch.GetValueOrDefault(item.Id) ?? []; + } + + AttachPeople(dto, item, user, prefetchedPeople); } if (options.ContainsField(ItemFields.PrimaryImageAspectRatio)) @@ -742,12 +763,18 @@ namespace Emby.Server.Implementations.Dto /// <param name="dto">The dto.</param> /// <param name="item">The item.</param> /// <param name="user">The requesting user.</param> - private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null) + /// <param name="prefetchedPeople">People fetched in batch by the caller; when null the people are queried per item.</param> + private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null, IReadOnlyList<PersonInfo>? prefetchedPeople = null) { + // When rendering a page of items the caller batch-fetches people for every item up + // front and passes them in, avoiding one GetPeople query per item. Fall back to the + // per-item query for the single item path where no batch is available. + var source = prefetchedPeople ?? _libraryManager.GetPeople(item); + // Ordering by person type to ensure actors and artists are at the front. // This is taking advantage of the fact that they both begin with A // This should be improved in the future - var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue) + var people = source.OrderBy(i => i.SortOrder ?? int.MaxValue) .ThenBy(i => { if (i.IsType(PersonKind.Actor)) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 5db3b80386..19371f68d7 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -3537,6 +3537,12 @@ namespace Emby.Server.Implementations.Library return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes); } + /// <inheritdoc/> + public IReadOnlyDictionary<Guid, IReadOnlyList<PersonInfo>> GetPeopleByItems(IReadOnlyList<Guid> itemIds) + { + return _peopleRepository.GetPeopleByItems(itemIds); + } + public void UpdatePeople(BaseItem item, List<PersonInfo> people) { UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult(); |
