Skip to content

[Perf] tags() looks like a getter but performs one API fetch per tag, per call #51

Description

@egg82

What problem are you trying to solve?

Lib/src/main/java/me/egg82/arr/radarr/v3/schema/MovieResource.java:324

public @NotNull PVector<@NotNull Tag> tags() {
    List<@NotNull Tag> r = new ArrayList<>();
    for (int id : this.tags) {
        Tag t = api.fetch(Tag.class, id);

Every call does an api.fetch per tag. Same shape at SeriesResource.java:298, ArtistResource.java:251 and Author.java:122.

This only bites when SKIP_TAGS is set. All five updaters call it from exactly one place, gated:

if (!skipTags.isEmpty() && hasAnyTag(skipTags, m.resource().tags())) {

RadarrUpdater.java:133, SonarrUpdater.java:106, LidarrUpdater.java:103, ReadarrUpdater.java:109, WhisparrUpdater.java:133. With no SKIP_TAGS configured the short-circuit means tags() is never called at all - so this costs nothing on a default install and scales with items × tags on one that uses the feature. Measured at 30 ms/cycle at 3 tags/item, 70 ms at 9.

The part that makes it worse

fetch(Class, int, params) skips its own in-memory idCache under the default configuration. AbstractArrAPI.java:118-131:

if ((memoryCache == Tristate.AUTO && fileCache == Tristate.AUTO && !isCacheWritable())
    || (memoryCache == Tristate.AUTO && fileCache == Tristate.FALSE)
    || memoryCache == Tristate.TRUE) {
    // ... idCache.computeIfAbsent(...)
}
return fetchInternal(type, id, params);   // idCache bypassed

USE_FILE_CACHE and USE_MEMORY_CACHE both default to AUTO, so with a writable cache directory the whole condition is false and the memory cache is never consulted - every tag lookup goes to fetchInternal, which reads and parses files from disk. A tag referenced by twenty items in one cycle is read twenty times.

What would you like Fetcharr to do?

A method named like an accessor returns data the object already has, and a repeated lookup of the same small object is served from memory.

Resolve the relation at the boundary rather than in the DTO: fetch the tag list once per cycle, build a label to id map, and compare ints against the IntSet already on the resource. hasAnyTag only needs the comparison, not hydrated Tag objects.

The idCache bypass is worth looking at independently of that - the condition reads as though it were meant to enable the memory cache when the file cache is unavailable, and instead disables it whenever the file cache works.

More broadly, a DTO holding a reference to ArrAPI is what makes an accessor able to do file reads at all.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions