Skip to content

Improve performance when requesting availableContentScopes - #5999

Closed
fraxachun wants to merge 1 commit into
mainfrom
content-scopes-performance
Closed

Improve performance when requesting availableContentScopes#5999
fraxachun wants to merge 1 commit into
mainfrom
content-scopes-performance

Conversation

@fraxachun

@fraxachun fraxachun commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

The availableContentScopes were repeatedly recomputed for every row when querying users; this is fixed by the cache (per resolver request). Additionally, filtering out duplicate content scopes was very slow when using lodash.uniqWith with many content scopes, and was significantly sped up by implementing a custom algorithm (on my machine, with 2000 content scopes, from about 15 seconds down to 0.15 seconds)."

@nsams

nsams commented Jul 15, 2026

Copy link
Copy Markdown
Member

Please add a description what this PR improves and by how much. And how caching (clearing) works.

@fraxachun
fraxachun force-pushed the content-scopes-performance branch from 0859e39 to 342461e Compare July 15, 2026 08:21
private availableContentScopesCache: ContentScopeWithLabel[] | undefined;

// Populates the cache used by getAvailableContentScopes() for the duration of one operation (e.g. resolving a list of users).
// Must be paired with clearAvailableContentScopesCache() so later, unrelated requests don't see a stale snapshot.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this cache isn't user specific, does it really need to be per request?

And if it is per request, you should use a Request scoped Service for that. The clear logic (called in a finally) doesn't work for multiple running requests, they all share a single service instance.

(also, a explicit warmupCache and clearCache isn't a easy to use api design)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this cache isn't user specific, does it really need to be per request?

I assume that you never want to risk this cache to be outdated because scopes could be added dynamically. So only making it per request makes sense to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has to be per request. However, I refactored only this change into a data loader in a new PR: #6113

"@comet/cms-api": patch
---

Improve performance when requesting `availableContentScopes`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would also be nice to have some more info here about what was actually changed

private availableContentScopesCache: ContentScopeWithLabel[] | undefined;

// Populates the cache used by getAvailableContentScopes() for the duration of one operation (e.g. resolving a list of users).
// Must be paired with clearAvailableContentScopesCache() so later, unrelated requests don't see a stale snapshot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this cache isn't user specific, does it really need to be per request?

I assume that you never want to risk this cache to be outdated because scopes could be added dynamically. So only making it per request makes sense to me.

@VPS-Obi VPS-Obi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loading 2000 content scopes per request is still a lot, do we want to support that many content scopes in the user permissions module?

@fraxachun

Copy link
Copy Markdown
Contributor Author

Loading 2000 content scopes per request is still a lot, do we want to support that many content scopes in the user permissions module?

We don't want to have 2000 content scopes. However, these changes are a quick win since this is a regression problem in the current comet version. Since there are applications with that many content scopes out there they are currently facing a performance problem when upgrading to new most recent comet version,.

@fraxachun

Copy link
Copy Markdown
Contributor Author

Split into #6113 and #6112

@fraxachun fraxachun closed this Aug 5, 2026
@VPS-Obi
VPS-Obi deleted the content-scopes-performance branch August 5, 2026 07:59
VPS-Obi pushed a commit that referenced this pull request Aug 6, 2026
…ader (#6113)

## Problem

When the admin queries the users list (`userPermissionsUsers`), the
`contentScopesCount` field resolver runs **once per row**. Each
invocation calls `getContentScopes(user)`, which recomputes the full
list of available content scopes (build labels + `uniqWith`
deduplication) from scratch. For a page of users this repeats the same
shared computation N times, which becomes very slow when a project
configures many content scopes.

## Solution

Resolve the available content scopes **once per request** by routing
`contentScopesCount` through a request-scoped `DataLoader`
(`UserContentScopesLoaderService`), following the existing DataLoader
pattern in cms-api (e.g. `AttachedDocumentLoaderService`). All users of
a request are batched into a single `getContentScopesForUsers(users)`
call that computes the available content scopes only once and then maps
each user's scopes.

This PR **keeps the existing `lodash.uniqWith` deduplication** unchanged
— the only change is *how often* the available content scopes are
computed.

## Speed comparison (25 users)

Measured by resolving `contentScopesCount` for 25 users (median of 3
runs), comparing the current per-row behavior against the DataLoader:

**Typical case — each user resolves to a few content scopes**

| Available content scopes | Current (per row) | DataLoader | Speedup |
| ------------------------ | ----------------- | ---------- | ------- |
| 250                      | 359 ms            | 15 ms      | ~25×    |
| 500                      | 1 377 ms          | 55 ms      | ~25×    |
| 1 000                    | 5 400 ms          | 209 ms     | ~26×    |
| 2 000                    | 20 738 ms         | 821 ms     | ~25×    |

**Worst case — every user resolves to *all* content scopes** (the
per-user `uniqWith`, which this PR does not change, then dominates in
both variants):

| Available content scopes | Current (per row) | DataLoader | Speedup |
| ------------------------ | ----------------- | ---------- | ------- |
| 250                      | 558 ms            | 268 ms     | ~2.1×   |
| 500                      | 2 229 ms          | 998 ms     | ~2.2×   |
| 1 000                    | 9 305 ms          | 4 075 ms   | ~2.3×   |
| 2 000                    | 35 057 ms         | 15 771 ms  | ~2.2×   |

So the DataLoader removes 24 of 25 redundant available-content-scope
computations per request; the remaining cost is the per-user work, which
is unchanged.

## Example / test

`user-permissions.service.spec.ts` covers the batching guarantee
(available content scopes computed only once for all users) and that
each user's scopes are returned in input order.

## Notes

Supersedes #5999 (which additionally replaced the `uniqWith`
deduplication with a custom algorithm). This PR isolates just the
DataLoader change.

https://claude.ai/code/session_01RGR2hkqpqcXL4xk7rGqz7n

Co-authored-by: Claude <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

None yet

Development

Successfully merging this pull request may close these issues.

4 participants