/Items/Filters2 and /Items/Latest are slow on a large library #17

Closed
opened 2026-08-02 14:27:24 +02:00 by Claude · 1 comment
Collaborator

Warm, stable across repeats, against 11 946 items:

endpoint time
/Items/Filters2 (Shows) 2.17–2.67 s
/Items/Latest (Shows) 1.27–1.40 s
/Items page 1 (Shows) 0.42 s
/Items/Counts 0.33 s
/Items startIndex=5000 0.09 s

Both slow ones are on the path the web client hits when opening a library, and
both scale with library size where paging does not — the deep page is the
fastest call in the table.


Found by scanning /mnt/media (285 movies, 289 shows, 74 music artists —
~13 000 media files on a CIFS mount) with both this server and upstream
Jellyfin at the pinned commit, remote metadata and image fetchers disabled on
upstream so both were reading files only. Upstream was given the same mount
path so MD5(UTF-16LE(path)) agrees and rows compare directly.

Warm, stable across repeats, against 11 946 items: | endpoint | time | |---|---| | `/Items/Filters2` (Shows) | 2.17–2.67 s | | `/Items/Latest` (Shows) | 1.27–1.40 s | | `/Items` page 1 (Shows) | 0.42 s | | `/Items/Counts` | 0.33 s | | `/Items` startIndex=5000 | 0.09 s | Both slow ones are on the path the web client hits when opening a library, and both scale with library size where paging does not — the deep page is the fastest call in the table. --- Found by scanning `/mnt/media` (285 movies, 289 shows, 74 music artists — ~13 000 media files on a CIFS mount) with both this server and upstream Jellyfin at the pinned commit, remote metadata and image fetchers disabled on upstream so both were reading files only. Upstream was given the same mount path so `MD5(UTF-16LE(path))` agrees and rows compare directly.
Author
Collaborator

Fixed in 55d5a3c, afd47eb and 7dcbaa2.

Measured on the same library, warm:

endpoint before after
/Items/Filters2 (Shows) 2170–2670 ms 105 ms
/Items/Latest (Shows) 1270–1400 ms 126 ms
/Items/Latest (Music) 1132 ms 9 ms
/Items page 1 (Shows) 5486 ms 51 ms
/Items/Counts 330 ms 395 ms

Three things were wrong, and the third was the largest.

A filter read the library twice over. /Items/Filters2 read every item and
then every item's streams — a query per item, per stream kind — to answer with
the languages they declare. Upstream asks the database once, and so does this
now.

A latest list read the library to report ten of its items. Both grouped
forms read pages until what is left cannot change the answer: as many keys as
are being reported for a film, and a series' window past that for a show, which
is upstream's own cutoff.

A query aimed at one folder read the whole table. Counting what a season
holds drove from BaseItems and probed the ancestors per row — 56 ms an item
against 0.3 ms for a film, so a page of a hundred series took five seconds.
Reading the ancestors first is 50x on that count, and it is what took /Items
on a show library from 5.5 s to 51 ms. Nothing in the issue named that endpoint;
it was the same defect seen from another side.

/Items/Counts is unchanged and now the slowest of the five.


Three correctness differences turned up in the same functions and are fixed
with them:

  • a stream hangs off an episode rather than the series above it, so a show
    library declared no languages at all. It declares 32 audio and 95
    subtitle languages now.
  • a stream that declares none is und — a language a client filters by — not
    nothing, and the list is ordered by the name a client shows rather than by
    the code it filters by.
  • upstream asks for no language where the request narrowed to something no
    stream hangs off, and never fills Tags at all.

Review caught two more in the new code: a stop that counts groups is only sound
if the read is ordered the way the answer is picked, which for a film it is not
— it reads past the moment the last reported key was added now. And
Limit=0 indexed off the end of an empty list.

Fixed in `55d5a3c`, `afd47eb` and `7dcbaa2`. **Measured on the same library, warm:** | endpoint | before | after | |---|---|---| | `/Items/Filters2` (Shows) | 2170–2670 ms | **105 ms** | | `/Items/Latest` (Shows) | 1270–1400 ms | **126 ms** | | `/Items/Latest` (Music) | 1132 ms | **9 ms** | | `/Items` page 1 (Shows) | 5486 ms | **51 ms** | | `/Items/Counts` | 330 ms | 395 ms | Three things were wrong, and the third was the largest. **A filter read the library twice over.** `/Items/Filters2` read every item and then every item's streams — a query per item, per stream kind — to answer with the languages they declare. Upstream asks the database once, and so does this now. **A latest list read the library to report ten of its items.** Both grouped forms read pages until what is left cannot change the answer: as many keys as are being reported for a film, and a series' window past that for a show, which is upstream's own cutoff. **A query aimed at one folder read the whole table.** Counting what a season holds drove from `BaseItems` and probed the ancestors per row — 56 ms an item against 0.3 ms for a film, so a page of a hundred series took five seconds. Reading the ancestors first is 50x on that count, and it is what took `/Items` on a show library from 5.5 s to 51 ms. Nothing in the issue named that endpoint; it was the same defect seen from another side. `/Items/Counts` is unchanged and now the slowest of the five. --- Three correctness differences turned up in the same functions and are fixed with them: - a stream hangs off an episode rather than the series above it, so a show library declared **no languages at all**. It declares 32 audio and 95 subtitle languages now. - a stream that declares none is `und` — a language a client filters by — not nothing, and the list is ordered by the name a client shows rather than by the code it filters by. - upstream asks for no language where the request narrowed to something no stream hangs off, and never fills `Tags` at all. Review caught two more in the new code: a stop that counts groups is only sound if the read is ordered the way the answer is picked, which for a film it is not — it reads past the moment the last reported key was added now. And `Limit=0` indexed off the end of an empty list.
Sign in to join this conversation.
No description provided.