Skip to content

Batch people lookups when building item DTOs - #17571

Merged
crobibero merged 1 commit into
jellyfin:masterfrom
obiwantoby:perf/batch-people-dto
Aug 8, 2026
Merged

crobibero merged 1 commit into
jellyfin:masterfrom
obiwantoby:perf/batch-people-dto

Conversation

@obiwantoby

Copy link
Copy Markdown

Problem

While fixing a playlist performance regression in Finamp (a Jellyfin client), I traced the slowness to the server: requesting
Fields=People on a list endpoint issues one people query per item. Fixing the client led me to look at Jellyfin itself.

DtoService.GetBaseItemDtos already batch fetches user data, child counts, played counts and artists before its per item loop, but AttachPeople still ran one _libraryManager.GetPeople(item) query per item. Rendering a page of items with the People field enabled (a playlist, a library grid, any /Items call requesting people) therefore fires one extra database round trip per row.

This is an N+1: a page of 100 items issues 100 separate people queries on top of the main item query.

Change

Add GetPeopleByItems(itemIds) to IPeopleRepository, which reads every requested item in a single query over the people mapping table and returns full PersonInfo (name, 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 now reads from the batch instead of querying per item.

The single item GetBaseItemDto path is unchanged: when no batch is supplied AttachPeople falls back to the existing per item query, so callers that render one item keep their current behaviour.

Correctness

The batched query mirrors the ordering and mapping of the existing single item people query (ordered by cast list order, mapped to PersonInfo with the same role, type and sort order). Verified on a live 12.0.0 server: the /Items responses with Fields=People are byte identical before and after this change, so the output is unchanged and only the query count drops.

Measured impact

Live server, median of six timed requests per endpoint after a warmup:

Endpoint Items Before After
Audio + People 300 1489 ms 233 ms
Movies + People 196 1998 ms 1048 ms
Episodes + People 200 1498 ms 606 ms
Movies browse, no People (control) 100 62 ms 63 ms

The control path that does not request people is unaffected, confirming the win comes from removing the per item people round trips.

Tests

Adds a DtoService test asserting people resolve from the batch and that the per item GetPeople is never called, modeled on the existing artist batch regression test. The full Jellyfin.Server.Implementations.Tests suite passes locally.

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

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

@Shadowghost

Copy link
Copy Markdown
Contributor

Please fill out the PR template

@gnattu

gnattu commented Aug 7, 2026

Copy link
Copy Markdown
Member

LGTM from code wise, but yes, please fill our template.

@jellyfin-bot

Copy link
Copy Markdown
Contributor

Jellyfin API (v 12.0.0)


Result


No differences. Specifications are equivalent

@obiwantoby

Copy link
Copy Markdown
Author

Changes

This adds GetPeopleByItems(itemIds) to IPeopleRepository, which reads all requested items in one query and returns full PersonInfo grouped by item id. GetBaseItemDtos prefetches this once when the People field is requested and hands it to AttachPeople. The single item GetBaseItemDto path is unchanged: with no batch supplied it falls back to the existing per item query.

I checked the output on a live 12.0.0 server and the /Items responses with Fields=People are byte identical before and after, so only the query count changes. Timing on that server (median of six requests after a warmup):

Endpoint Items Before After
Audio + People 300 1489 ms 233 ms
Movies + People 196 1998 ms 1048 ms
Episodes + People 200 1498 ms 606 ms
Movies, no People (control) 100 62 ms 63 ms

The control path that does not request people is unaffected. Adds a DtoService test asserting people resolve from the batch and that the per item GetPeople is never called. Full Jellyfin.Server.Implementations.Tests suite passes locally.

Code assistance

Used a LLM for the benchmarking harness and profiling guidance while tracking down the N+1.

Issues

Traced this while fixing a related playlist slowness in Finamp (finamp-app/finamp#1707).

@theguymadmax theguymadmax moved this from Todo to Approved in Jellyfin v12 Aug 7, 2026
@crobibero
crobibero merged commit 247ee40 into jellyfin:master Aug 8, 2026
15 of 16 checks passed
@github-project-automation github-project-automation Bot moved this from Approved to Done in Jellyfin v12 Aug 8, 2026
vdatanet added a commit to vdatanet/jellyfin that referenced this pull request Aug 19, 2026
…hHDR10Plus

ShouldRemoveDynamicHdrMetadata stripped the HDR10+ SEI from every
DOVIWithHDR10Plus stream copy as soon as the client declared DOVI in its
rangetype, without considering that the client may have declared
DOVIWithHDR10Plus support itself. The rewritten bitstream
(-bsf:v hevc_mp4toannexb,hevc_metadata=remove_hdr10plus=1) breaks HLS fMP4
playback on AVPlayer (tvOS), and no device profile combination avoids it
while keeping Dolby Vision remuxing, because the dvh1 tagging in
DynamicHlsController requires DOVI in the rangetype.

Honor an explicit DOVIWithHDR10Plus declaration the same way the
DOVIWithELHDR10Plus coexistence case already honors its own: only plan the
HDR10+ removal for DOVI clients that did not declare the coexistence range.
Clients that do not declare it keep the previous behavior.

No upstream fix exists as of master (post-jellyfin#17571); the removal logic is
unchanged since it was introduced in jellyfin#13277. Related upstream issue: jellyfin#16687.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants