Skip to content

Commit

Permalink
perf(dashboard): load items from DB instead of using file sys abstrac…
Browse files Browse the repository at this point in the history
…tion

- adds a simplified RecentPage model to avoid incomplete PageInfo instances
- add RecentPageService with a method to fetch them for a specified user
- adapts and simplifies RecenPagesWidget

Signed-off-by: Arthur Schiwon <[email protected]>
  • Loading branch information
blizzz committed Sep 29, 2023
1 parent 8908e1d commit 41618ee
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 51 deletions.
60 changes: 9 additions & 51 deletions lib/Dashboard/RecentPagesWidget.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

namespace OCA\Collectives\Dashboard;

use OCA\Collectives\Model\CollectiveInfo;
use OCA\Collectives\Model\PageInfo;
use OCA\Collectives\Service\CollectiveService;
use OCA\Collectives\Service\PageService;
use OCA\Collectives\Service\RecentPagesService;
use OCP\Dashboard\IReloadableWidget;
use OCP\Dashboard\Model\WidgetItem;
use OCP\Dashboard\Model\WidgetItems;
Expand All @@ -23,65 +22,24 @@ public function __construct(
protected IUserSession $userSession,
protected PageService $pageService,
protected CollectiveService $collectiveService,
protected RecentPagesService $recentPagesService,
) {}

public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems {
if (!($user = $this->userSession->getUser())) {
return new WidgetItems();
}

$collectives = $this->collectiveService->getCollectives($user->getUID());
$results = [];
foreach ($collectives as $collective) {
// fetch pages from the current collective
$id = $collective->getId();
$pages = $this->pageService->findAll($id, $user->getUID());

// sort pages and slice to the maximal necessary amount
usort($pages, function (PageInfo $a, PageInfo $b): int {
return $b->getTimestamp() - $a->getTimestamp();
});
$pages = array_slice($pages, 0, self::MAX_ITEMS);

// prepare result entries
foreach ($pages as $page) {
$results[] = [
'timestamp' => $page->getTimestamp(),
'page' => $page,
'collective' => $collective
];
}

// again sort result and slice to the max amount
usort($results, function (array $a, array $b): int {
return $b['timestamp'] - $a['timestamp'];
});
$results = array_slice($results, 0, self::MAX_ITEMS);
}
$recentPages = $this->recentPagesService->forUser($user, self::MAX_ITEMS);

$items = [];
foreach ($results as $result) {
/* @var array{timestamp: int, page: PageInfo, collective: CollectiveInfo} $result */

$pathParts = [$result['collective']->getName()];
if ($result['page']->getFilePath() !== '') {
$pathParts = array_merge($pathParts, explode('/', $result['page']->getFilePath()));
}
if ($result['page']->getFileName() !== 'Readme.md') {
$pathParts[] = $result['page']->getTitle();
}

$iconData = $result['page']->getEmoji()
? $this->getEmojiAvatar($result['page']->getEmoji())
: $this->getEmojiAvatar('🗒');
//: $this->getFallbackDataIcon();

foreach ($recentPages as $recentPage) {
$items[] = new WidgetItem(
$result['page']->getTitle(),
$result['collective']->getName(),
$this->urlGenerator->linkToRoute('collectives.start.indexPath', ['path' => implode('/', $pathParts)]),
'data:image/svg+xml;base64,' . base64_encode($iconData),
(string)$result['timestamp']
$recentPage->getTitle(),
$recentPage->getCollectiveName(),
$recentPage->getPageUrl(),
'data:image/svg+xml;base64,' . base64_encode($this->getEmojiAvatar($recentPage->getEmoji())),
(string)$recentPage->getTimestamp()
);
}

Expand Down
56 changes: 56 additions & 0 deletions lib/Model/RecentPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace OCA\Collectives\Model;

class RecentPage {
protected string $collectiveName = '';
protected string $pageUrl = '';
protected string $title = '';
protected string $emoji = '🗒';
protected int $timestamp = 0;

public function getCollectiveName(): string {
return $this->collectiveName;
}

public function setCollectiveName(string $collectiveName): self {
$this->collectiveName = $collectiveName;
return $this;
}

public function getPageUrl(): string {
return $this->pageUrl;
}

public function setPageUrl(string $pageUrl): self {
$this->pageUrl = $pageUrl;
return $this;
}

public function getTitle(): string {
return $this->title;
}

public function setTitle(string $title): self {
$this->title = $title;
return $this;
}

public function getEmoji(): string {
return $this->emoji;
}

public function setEmoji(string $emoji): self {
$this->emoji = $emoji;
return $this;
}

public function getTimestamp(): int {
return $this->timestamp;
}

public function setTimestamp(int $timestamp): self {
$this->timestamp = $timestamp;
return $this;
}
}
118 changes: 118 additions & 0 deletions lib/Service/RecentPagesService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace OCA\Collectives\Service;

use OCA\Collectives\Model\PageInfo;
use OCA\Collectives\Model\RecentPage;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\IMimeTypeLoader;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IURLGenerator;
use OCP\IUser;

class RecentPagesService {

public function __construct(
protected CollectiveService $collectiveService,
protected IDBConnection $dbc,
protected IConfig $config,
protected IMimeTypeLoader $mimeTypeLoader,
protected IURLGenerator $urlGenerator,
) { }

/**
* @return RecentPage[]
* @throws MissingDependencyException
* @throws Exception
*/
public function forUser(IUser $user, int $limit = 10): array {
try {
$collectives = $this->collectiveService->getCollectives($user->getUID());
} catch (NotFoundException|NotPermittedException) {
return [];
}

$qb = $this->dbc->getQueryBuilder();
$appData = $this->getAppDataFolderName();
$mimeTypeMd = $this->mimeTypeLoader->getId('text/markdown');

$expressions = [];
foreach ($collectives as $collective) {
$value = sprintf($appData . '/collectives/%d/%%', $collective->getId());
$expressions[] = $qb->expr()->like('f.path', $qb->createNamedParameter($value, IQueryBuilder::PARAM_STR));
}

$qb->select('p.*', 'f.mtime as timestamp', 'f.name as filename', 'f.path as path')
->from('filecache', 'f')
->leftJoin('f', 'collectives_pages', 'p', $qb->expr()->eq('f.fileid', 'p.file_id'))
->where($qb->expr()->orX(...$expressions))
->andWhere($qb->expr()->eq('f.mimetype', $qb->createNamedParameter($mimeTypeMd, IQueryBuilder::PARAM_INT)))
->orderBy('f.mtime', 'DESC')
->setMaxResults($limit);

$r = $qb->executeQuery();

$pages = [];
$collectives = [];
while ($row = $r->fetch()) {
$collectiveId = (int)explode('/', $row['path'], 4)[2];
if (!isset($collectives[$collectiveId])) {
try {
// collectives are not cached, but always read from DB, so keep them
$collectives[$collectiveId] = $this->collectiveService->getCollectiveInfo($collectiveId, $user->getUID());
} catch (MissingDependencyException|NotFoundException|NotPermittedException) {
// just skip
continue;
}
}

// cut out $appDataDir/collectives/%d/ prefix from front, and filename at the rear
$splitPath = explode('/', $row['path'], 4);
$internalPath = dirname(array_pop($splitPath));
unset($splitPath);

// prepare link and title
$pathParts = [$collectives[$collectiveId]->getName()];
if ($internalPath !== '' && $internalPath !== '.') {
$pathParts = array_merge($pathParts, explode('/', $internalPath));
}
if ($row['filename'] !== 'Readme.md') {
$pathParts[] = basename($row['filename'], PageInfo::SUFFIX);
$title = basename($row['filename'], PageInfo::SUFFIX);
} else {
$title = basename($internalPath);
}
$url = $this->urlGenerator->linkToRoute('collectives.start.indexPath', ['path' => implode('/', $pathParts)]);

// build result model
// not returning a PageInfo instance because it would be either incomplete or too expensive to build completely
$recentPage = new RecentPage();
$recentPage->setCollectiveName($this->collectiveService->getCollectiveNameWithEmoji($collectives[$collectiveId]));
$recentPage->setTitle($title);
$recentPage->setPageUrl($url);
$recentPage->setTimestamp($row['timestamp']);
if ($row['emoji']) {
$recentPage->setEmoji($row['emoji']);
}

$pages[] = $recentPage;
}
$r->closeCursor();

return $pages;
}

private function getAppDataFolderName(): string {
$instanceId = $this->config->getSystemValueString('instanceid', '');
if ($instanceId === '') {
throw new \RuntimeException('no instance id!');
}

return 'appdata_' . $instanceId;
}

}

0 comments on commit 41618ee

Please sign in to comment.