Skip to content

Performance: Coalesce concurrent tree data requests (Management API client) - #23021

Merged
iOvergaard merged 2 commits into
v17/devfrom
v17/improvement/coalesce-tree-requests
Jun 4, 2026
Merged

Performance: Coalesce concurrent tree data requests (Management API client)#23021
iOvergaard merged 2 commits into
v17/devfrom
v17/improvement/coalesce-tree-requests

Conversation

@iOvergaard

@iOvergaard iOvergaard commented May 29, 2026

Copy link
Copy Markdown
Contributor

What

The backoffice tree data request manager (UmbManagementApiTreeDataRequestManager) hit the network on every call, with no in-flight dedup. So multiple concurrent consumers of the same tree — the sidebar tree, the breadcrumb/menu-structure context, pickers — each fired their own request. A document-workspace load fires tree/document/root three times, plus duplicate children/ancestors fetches.

This applies the existing UmbManagementApiInFlightRequestCache — already used by the item and detail request managers (webhook, language, user, templating, data-type, …) via a static #inflightRequestCache — to the tree request manager. Concurrent identical root/children/ancestors/siblings requests now share a single in-flight call.

Why

On localhost the duplicates are ~14 ms each (invisible); on high-latency hosts (e.g. Cloud) each is a full ~150 ms round-trip on the document-load critical path. This is the same dedup the item/detail managers already get — the tree manager was simply the one place the pattern was never applied.

Design

  • In-flight only. The cache entry is removed once the promise settles (finally), so it coalesces concurrent requests and never serves stale data. No cache invalidation concerns.
  • Behind the existing seam. No public/extension API change — consumers still call requestTreeRootItems etc.
  • Opt-in per tree. The base accepts an optional inflightRequestCache; only the document tree wires the shared static cache in this PR. Other trees (media, members, data-types…) are unchanged until they pass one — a natural, low-risk follow-up.

How to test

  1. Open a document workspace; observe the network panel — tree/document/root should fire once instead of three times.
  2. Expand/collapse tree nodes and confirm tree behaviour is unchanged (data still loads, pagination/siblings still work).

Notes / follow-ups

  • No unit tests added yet — the coalescing helper is a good candidate for a focused concurrency test (two simultaneous getRootItems → one underlying call). Happy to add if wanted.
  • Wiring the same shared cache into the other trees (media/members/etc.) is a trivial follow-up that removes their duplicates too.
  • Part of the backoffice boot/load perf work; independent of the boot-parallelization PR.

Related to #21152

🤖 Generated with Claude Code

The tree data request manager hit the network on every call, so multiple
concurrent consumers (sidebar tree, breadcrumb structure, pickers) each
fetched the same data independently — e.g. three identical tree/document/root
requests per document-workspace load.

Apply the existing UmbManagementApiInFlightRequestCache (already used by the
item and detail request managers) to the tree request manager via a shared
static cache, coalescing concurrent identical root/children/ancestors/siblings
calls into a single in-flight request, cleared on settle (in-flight only, so
no stale-cache risk). The document tree opts in; other trees are unchanged
until they pass a cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 29, 2026 16:13
@claude

claude Bot commented May 29, 2026

Copy link
Copy Markdown

Claude finished @iOvergaard's task in 3m 45s —— View job


PR Review

Target: origin/v17/dev · Based on commit: 01ef4225 · Skipped: 0 noise files out of 2 total

Applies the existing UmbManagementApiInFlightRequestCache — already used by item/detail request managers — to the tree request manager via a new private #coalesce method. Concurrent identical root/children/ancestors/siblings requests now share one in-flight call; the cache entry is removed once the promise settles so no stale data concerns arise. Only the document tree is wired in this PR; other trees are unchanged.

  • Modified public API: UmbManagementApiTreeDataRequestManagerArgs.inflightRequestCache (optional field added — backward-compatible)
  • No breaking changes — field is optional; all existing consumers compile unchanged.

Important

  • tree-data.request-manager.ts — No tests added for the #coalesce logic. Both sibling managers in the same package (detail-data.request-manager.test.ts, item-data.request-manager.test.ts) have focused test files covering the in-flight dedup behavior: concurrent calls share one request, error propagates to all awaiters, and the cache entry is removed on settle. The same coverage is a natural fit here and would protect this logic from regressions. The PR author acknowledges this as a follow-up — worth tracking. Fix this →

Suggestions

  • tree-data.request-manager.ts:80–82 — The 3-line comment block on #coalesce violates the CLAUDE.md code comment policy ("Never write multi-paragraph docstrings or multi-line comment blocks — one short line max"). The non-obvious WHY (in-flight only, no stale data risk) fits in one line:

    // In-flight only — dedupes concurrent identical requests; entry removed on settle.
    async #coalesce<ResultType>(key: string, request: () => Promise<ResultType>): Promise<ResultType> {
  • tree-data.request-manager.ts:37 — New inflightRequestCache? field on the exported UmbManagementApiTreeDataRequestManagerArgs interface has no JSDoc. Coding preferences require documentation on all public/exported APIs. Suggestion:

    /**
     * Optional in-flight deduplication cache. When provided, concurrent identical requests
     * share a single in-flight call instead of each hitting the network independently.
     */
    inflightRequestCache?: UmbManagementApiInFlightRequestCache<unknown>;
  • tree-data.request-manager.ts:89–95 — The #coalesce generic ResultType is unconstrained, requiring promise as Promise<UmbApiResponse<{ data?: unknown }>> to satisfy cache.set(). Since all call sites pass a tryExecute(...) result, constraining ResultType extends UmbApiResponse<{ data?: unknown }> would remove the cast and make the assumption explicit at the signature level:

    async #coalesce<ResultType extends UmbApiResponse<{ data?: unknown }>>(
        key: string,
        request: () => Promise<ResultType>,
    ): Promise<ResultType> {
        // ...
        cache.set(key, promise); // no cast needed

Approved with Suggestions for improvement

Good to go, but please carefully consider the importance of the suggestions.


Labels applied: area/frontend, category/performance

@claude claude Bot added area/frontend category/performance Fixes for performance (generally cpu or memory) fixes labels May 29, 2026

Copilot AI 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.

Pull request overview

This PR reduces duplicate concurrent backoffice tree-data calls by introducing in-flight request coalescing in UmbManagementApiTreeDataRequestManager, and wiring it up for the document tree so multiple consumers share the same in-flight Management API request.

Changes:

  • Added optional inflightRequestCache support to UmbManagementApiTreeDataRequestManager and implemented a #coalesce() helper to deduplicate identical concurrent root/children/ancestors/siblings requests.
  • Wired the document tree request manager to use a shared static UmbManagementApiInFlightRequestCache to coalesce concurrent document tree requests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
src/Umbraco.Web.UI.Client/src/packages/management-api/tree/tree-data.request-manager.ts Adds in-flight coalescing logic and optional cache wiring for tree requests.
src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/server-data-source/document-tree.server.request-manager.ts Provides a shared static in-flight cache instance for document tree requests.

- Add focused tests: concurrent identical root requests share one call,
  the in-flight entry is cleared on settle, and no cache means no coalescing.
- Build the cache key lazily (only when a cache is wired) so non-opted-in
  trees keep the original lightweight path.
- Constrain the #coalesce generic to drop the cast on cache.set.
- Document the new inflightRequestCache arg; trim the comment to one line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@iOvergaard

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all addressed in f146412:

  • Tests — added tree-data.request-manager.test.ts (concurrent dedup → one call, entry cleared on settle, no-cache → no coalescing). All passing.
  • Generic constraint#coalesce<ResultType extends UmbApiResponse<{ data?: unknown }>>, which removes the cache.set cast.
  • Comment — trimmed to a single line per the comment policy.
  • JSDoc — added on the new inflightRequestCache arg.
  • Lazy key (the 5 inline notes) — #coalesce now takes a key factory invoked only when a cache is wired, so non-opted-in trees keep the original path.

Also fixed an import/order slip (parent import was after the sibling import) introduced by the first commit.

@iOvergaard iOvergaard changed the title Backoffice: Coalesce concurrent tree data requests (Management API client) Performance: Coalesce concurrent tree data requests (Management API client) Jun 1, 2026

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

This looks to work as expected. With a hard reload on a page for a document in the backoffice I see the request for /umbraco/management/api/v1/tree/document/root?skip=0&take=0 only fired once. From v17/dev it's fired twice (though not three times as stated in the PR description - there is a third call, but it has a different take value, and that's correctly retained as a separate request with this PR).

Will leave to you to merge or reach out for a more FE expert review if you feel you need one (but given it's following existing patterns, seems reasonable to proceed with to me).

@iOvergaard

Copy link
Copy Markdown
Contributor Author

@AndyButland, yeah, different take values will cause that for you. Perhaps we could optimise the request manager to be more intelligent to understand "skip" and "take" and collate them if the range is within the bounds of an already fired request.

@iOvergaard
iOvergaard merged commit ad90db8 into v17/dev Jun 4, 2026
42 checks passed
@iOvergaard
iOvergaard deleted the v17/improvement/coalesce-tree-requests branch June 4, 2026 08:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/frontend category/performance Fixes for performance (generally cpu or memory) fixes release/17.6.0 release/18.1.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants