-
-
Notifications
You must be signed in to change notification settings - Fork 5.2k
perf: Load entries lazily again in NavigationManager #64109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
come-nc
wants to merge
1
commit into
master
Choose a base branch
from
perf/navigation-manager
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+104
−63
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -54,23 +54,31 @@ class NavigationManager implements INavigationManager { | |||||
| 'activity' => -88, | ||||||
| ]; | ||||||
|
|
||||||
| protected ?string $activeEntry = null; | ||||||
| /** @var array<string, NavigationEntryOutput> */ | ||||||
| protected array $entries = []; | ||||||
| /** @var list<NavigationEntry> */ | ||||||
| private array $newEntries = []; | ||||||
| /** @var list<callable(): ?NavigationEntry> */ | ||||||
| protected array $closureEntries = []; | ||||||
| private array $closureEntries = []; | ||||||
|
|
||||||
| private ?string $defaultEntryId = null; | ||||||
|
|
||||||
| private ?string $activeEntry = null; | ||||||
| /** @var array<string, NavigationEntryOutput> */ | ||||||
| private array $entries = []; | ||||||
| /** User defined app order (cached for the `add` function) */ | ||||||
| protected ?array $customAppOrder = null; | ||||||
| private ?array $customAppOrder = null; | ||||||
| /** @var array<string, int> */ | ||||||
| protected array $unreadCounters = []; | ||||||
| private array $unreadCounters = []; | ||||||
|
|
||||||
| /** true if the internal state has been initialized */ | ||||||
| protected bool $initAppOrderDone = false; | ||||||
| private bool $initAppOrderDone = false; | ||||||
| /** true if all apps have been loaded by the App Manager */ | ||||||
| protected bool $initSetupDone = false; | ||||||
| private bool $initSetupDone = false; | ||||||
| /** List of loaded app info */ | ||||||
| private array $loadedAppInfo = []; | ||||||
|
|
||||||
| private ?bool $isAdmin = null; | ||||||
| private bool $eventFired = false; | ||||||
|
|
||||||
| public function __construct( | ||||||
| protected IAppManager $appManager, | ||||||
| private IURLGenerator $urlGenerator, | ||||||
|
|
@@ -89,12 +97,20 @@ public function add(array|callable $entry): void { | |||||
| $this->closureEntries[] = $entry; | ||||||
| return; | ||||||
| } | ||||||
| $this->newEntries[] = $entry; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * @param NavigationEntry $entry | ||||||
| */ | ||||||
| private function addEntry($entry): void { | ||||||
| // if needed initialize the internal state to allow setting app order and default app | ||||||
| $this->initCustomAppOrder(); | ||||||
|
|
||||||
| $id = $entry['id']; | ||||||
|
|
||||||
| $entry['active'] = false; | ||||||
| $entry['default'] = false; | ||||||
| $entry['unread'] = $this->unreadCounters[$id] ?? 0; | ||||||
| if (!isset($entry['icon'])) { | ||||||
| $entry['icon'] = ''; | ||||||
|
|
@@ -120,18 +136,10 @@ public function add(array|callable $entry): void { | |||||
| } | ||||||
|
|
||||||
| $this->entries[$id] = $entry; | ||||||
|
|
||||||
| // Needs to be done after adding the new entry to account for the default entries containing this new entry. | ||||||
| $this->updateDefaultEntries(); | ||||||
| } | ||||||
|
|
||||||
| private function updateDefaultEntries(): void { | ||||||
| $defaultEntryId = $this->getDefaultEntryIdForUser($this->userSession->getUser(), false); | ||||||
| foreach ($this->entries as $id => $entry) { | ||||||
| if ($entry['type'] === 'link') { | ||||||
| $this->entries[$id]['default'] = $id === $defaultEntryId; | ||||||
| } | ||||||
| } | ||||||
| $this->defaultEntryId = $this->getDefaultEntryIdForUser($this->userSession->getUser(), false); | ||||||
| } | ||||||
|
|
||||||
| #[Override] | ||||||
|
|
@@ -155,23 +163,29 @@ public function getAll(string $type = 'link'): array { | |||||
| * @return array<string, NavigationEntryOutput> | ||||||
| */ | ||||||
| private function proceedNavigation(array $list, string $type): array { | ||||||
| $noDefault = true; | ||||||
| if ($this->defaultEntryId !== null && isset($list[$this->defaultEntryId])) { | ||||||
| $list[$this->defaultEntryId]['default'] = true; | ||||||
| $noDefault = false; | ||||||
| } | ||||||
|
|
||||||
| uasort($list, function ($a, $b) { | ||||||
| if (($a['default'] ?? false) xor ($b['default'] ?? false)) { | ||||||
| if ($a['default'] xor $b['default']) { | ||||||
| // Always sort the default app first | ||||||
| return ($a['default'] ?? false) ? -1 : 1; | ||||||
| return $a['default'] ? -1 : 1; | ||||||
| } elseif (isset($a['order']) && isset($b['order'])) { | ||||||
| // Sort by order | ||||||
| return ($a['order'] < $b['order']) ? -1 : 1; | ||||||
| return $a['order'] <=> $b['order']; | ||||||
| } elseif (isset($a['order']) || isset($b['order'])) { | ||||||
| // Sort the one that has an order property first | ||||||
| return isset($a['order']) ? -1 : 1; | ||||||
| } else { | ||||||
| // Sort by name otherwise | ||||||
| return ($a['name'] < $b['name']) ? -1 : 1; | ||||||
| return $a['name'] <=> $b['name']; | ||||||
| } | ||||||
| }); | ||||||
|
|
||||||
| if ($type === 'all' || $type === 'link') { | ||||||
| if ($noDefault && ($type === 'all' || $type === 'link')) { | ||||||
| // There might be the case that no default app was set, in this case the first app is the default app. | ||||||
| // Otherwise, the default app is already the ordered first, so setting the default prop will make no difference. | ||||||
| foreach ($list as $index => &$navEntry) { | ||||||
|
|
@@ -184,15 +198,8 @@ private function proceedNavigation(array $list, string $type): array { | |||||
| } | ||||||
|
|
||||||
| $activeEntry = $this->getActiveEntry(); | ||||||
| if ($activeEntry !== null) { | ||||||
| foreach ($list as $index => &$navEntry) { | ||||||
| if ($navEntry['id'] == $activeEntry) { | ||||||
| $navEntry['active'] = true; | ||||||
| } else { | ||||||
| $navEntry['active'] = false; | ||||||
| } | ||||||
| } | ||||||
| unset($navEntry); | ||||||
| if ($activeEntry !== null && isset($list[$activeEntry])) { | ||||||
| $list[$activeEntry]['active'] = true; | ||||||
| } | ||||||
|
|
||||||
| return $list; | ||||||
|
|
@@ -204,6 +211,9 @@ private function proceedNavigation(array $list, string $type): array { | |||||
| public function clear(bool $resetInit = true): void { | ||||||
| $this->entries = []; | ||||||
| $this->closureEntries = []; | ||||||
| $this->newEntries = []; | ||||||
| $this->defaultEntryId = null; | ||||||
| $this->activeEntry = null; | ||||||
|
|
||||||
| if ($resetInit) { | ||||||
| $this->loadedAppInfo = []; | ||||||
|
|
@@ -247,9 +257,6 @@ private function initCustomAppOrder(): void { | |||||
| * @internal - This is only used by Nextcloud core to setup the navigation manager. It is not intended for use by apps. | ||||||
| */ | ||||||
| public function setup(): void { | ||||||
| // Resolve dynamically added navigation entries via event listeners | ||||||
| $this->eventDispatcher->dispatchTyped(new LoadAdditionalEntriesEvent()); | ||||||
|
|
||||||
| // mark setup as done to allow performance optimizations | ||||||
| $this->initSetupDone = true; | ||||||
| } | ||||||
|
|
@@ -263,13 +270,15 @@ public function setup(): void { | |||||
| * So we need to resolve the navigation entries here, even if not all apps are loaded yet. | ||||||
| */ | ||||||
| private function resolveAppNavigationEntries(): void { | ||||||
| if ($this->userSession->isLoggedIn()) { | ||||||
| $user = $this->userSession->getUser(); | ||||||
| $user = $this->userSession->getUser(); | ||||||
| if ($user !== null) { | ||||||
| $apps = $this->appManager->getEnabledAppsForUser($user); | ||||||
| } else { | ||||||
| $apps = $this->appManager->getEnabledApps(); | ||||||
| } | ||||||
|
|
||||||
| $this->isAdmin ??= $this->isAdmin(); | ||||||
|
|
||||||
| foreach ($apps as $app) { | ||||||
| if (in_array($app, $this->loadedAppInfo, true)) { | ||||||
| // already loaded | ||||||
|
|
@@ -279,12 +288,12 @@ private function resolveAppNavigationEntries(): void { | |||||
| // app is not loaded yet, skip it | ||||||
| continue; | ||||||
| } | ||||||
| $this->loadedAppInfo[] = $app; | ||||||
|
|
||||||
| // load plugins and collections from info.xml | ||||||
| $info = $this->appManager->getAppInfo($app); | ||||||
| if (!isset($info['navigations']['navigation'])) { | ||||||
| // this app does not have any navigation entries, skip it | ||||||
| $this->loadedAppInfo[] = $app; | ||||||
| continue; | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -298,7 +307,7 @@ private function resolveAppNavigationEntries(): void { | |||||
| continue; | ||||||
| } | ||||||
| $role = $nav['@attributes']['role'] ?? 'all'; | ||||||
| if ($role === 'admin' && !$this->isAdmin()) { | ||||||
| if ($role === 'admin' && !$this->isAdmin) { | ||||||
| continue; | ||||||
| } | ||||||
| $id = $nav['id'] ?? $app . ($key === 0 ? '' : $key); | ||||||
|
|
@@ -329,12 +338,11 @@ private function resolveAppNavigationEntries(): void { | |||||
| } | ||||||
|
|
||||||
| $l = $this->l10nFac->get($app); | ||||||
| $this->loadedAppInfo[] = $app; | ||||||
| $this->add(array_merge([ | ||||||
| // Navigation id | ||||||
| 'id' => $id, | ||||||
| // Order where this entry should be shown | ||||||
| 'order' => $order, | ||||||
| 'order' => (int)$order, | ||||||
| // Target of the navigation entry | ||||||
| 'href' => $route, | ||||||
| // The icon used for the navigation entry | ||||||
|
|
@@ -351,8 +359,16 @@ private function resolveAppNavigationEntries(): void { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| $updateDefaultEntries = false; | ||||||
|
|
||||||
| // once all apps are loaded we can resolve the app navigation closures | ||||||
| if ($this->initSetupDone) { | ||||||
| if (!$this->eventFired) { | ||||||
| // Resolve dynamically added navigation entries via event listeners | ||||||
| $this->eventDispatcher->dispatchTyped(new LoadAdditionalEntriesEvent()); | ||||||
| $this->eventFired = true; | ||||||
| } | ||||||
|
|
||||||
| // This has to be done on every call, | ||||||
| // as apps might add new navigation entries via closures at any time | ||||||
| while ($c = array_pop($this->closureEntries)) { | ||||||
|
|
@@ -362,12 +378,26 @@ private function resolveAppNavigationEntries(): void { | |||||
| $this->logger->debug('Closure of navigation entry returned null, skipping'); | ||||||
| continue; | ||||||
| } | ||||||
| $this->add($entry); | ||||||
| $this->addEntry($entry); | ||||||
| $updateDefaultEntries = true; | ||||||
| } catch (\Throwable $e) { | ||||||
| $this->logger->error('Failed to add navigation entry from closure', ['exception' => $e]); | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| while ($entry = array_pop($this->newEntries)) { | ||||||
| try { | ||||||
| $this->addEntry($entry); | ||||||
| $updateDefaultEntries = true; | ||||||
| } catch (\Throwable $e) { | ||||||
| $this->logger->error('Failed to add navigation entry from closure', ['exception' => $e]); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copy Pasta
Suggested change
|
||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if ($updateDefaultEntries) { | ||||||
| $this->updateDefaultEntries(); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private function isAdmin(): bool { | ||||||
|
|
@@ -386,12 +416,21 @@ public function setUnreadCounter(string $id, int $unreadCounter): void { | |||||
| #[Override] | ||||||
| public function get(string $id): ?array { | ||||||
| $this->resolveAppNavigationEntries(); | ||||||
| return $this->entries[$id] ?? null; | ||||||
| if (!isset($this->entries[$id])) { | ||||||
| return null; | ||||||
| } | ||||||
| $entry = $this->entries[$id]; | ||||||
| if ($this->defaultEntryId === $id) { | ||||||
| $entry['default'] = true; | ||||||
| } | ||||||
| if ($this->activeEntry === $id) { | ||||||
| $entry['active'] = true; | ||||||
| } | ||||||
| return $entry; | ||||||
| } | ||||||
|
|
||||||
| #[Override] | ||||||
| public function getDefaultEntryIdForUser(?IUser $user = null, bool $withFallbacks = true): string { | ||||||
| $this->resolveAppNavigationEntries(); | ||||||
| // Disable fallbacks here, as we need to override them with the user defaults if none are configured. | ||||||
| $defaultEntryIds = $this->getDefaultEntryIds(false); | ||||||
|
|
||||||
|
|
||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,7 +38,7 @@ | |
| * type: 'link'|'action'|'settings'|'guest'|'quota', | ||
| * name: string, | ||
| * app?: string, | ||
| * default?: bool, | ||
| * default: bool, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure this makes sense to add this to all output types (e.g. action) |
||
| * active: bool, | ||
| * classes: string, | ||
| * unread: int, | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think
addvsaddEntryis confusing its rather something like process here 👀