Skip to content

Commit 40b0b68

Browse files
authored
Merge pull request #4151 from nextcloud/ref/performance
Accelerate poll loading
2 parents 38f810d + 80f4cc2 commit 40b0b68

32 files changed

Lines changed: 463 additions & 243 deletions

.gitignore

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,55 @@
11
# SPDX-FileCopyrightText: 2016 Nextcloud contributors
22
# SPDX-License-Identifier: AGPL-3.0-or-later
3-
/assets/
3+
4+
# build dirs
5+
/assets
46
/css/*.map
5-
/js/*
6-
/css/*
7+
/js
8+
/css
9+
/build
10+
11+
# Mac nonsense
712
.DS_Store
13+
14+
# Win nonsense
15+
Thumbs.db
16+
17+
# local folders and files
18+
.local
19+
20+
# caches
821
.sass-cache/
922
.php_cs.cache
1023
.php-cs-fixer.cache
1124
.psalm.cache
25+
tests/.phpunit.result.cache
26+
27+
# logs
28+
npm-debug.log
29+
yarn-error.log
30+
31+
# IDEs
1232
.project/
1333
.idea/
1434
.vscode/
15-
build/
1635
nbproject/
36+
*.iml
37+
*.ntvs*
38+
*.suo
39+
*.sln
40+
*.njsproj
41+
42+
# modules
1743
node_modules/
18-
tests/.phpunit.result.cache
19-
npm-debug.log
44+
45+
# Composer vendors
2046
vendor
2147
vendor-bin/**/vendor/
2248
vendor-bin/**/*.lock
23-
update-workflows.sh
24-
yarn-error.log
25-
Thumbs.db
49+
50+
# local command files
51+
*.sh
2652
*.cmd
27-
*.env
28-
*.iml
29-
*.ntvs*
30-
*.njsproj
31-
*.sln
32-
*.suo
53+
54+
# misc
3355
tests/Api

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@ All notable changes to this project will be documented in this file.
2020
- Optimized janitor cron
2121
- Optimized rebuild command
2222
- Optimized poll loading by migrating subqueries to join (#3692)
23+
- Accelerated loading performance of polls
2324
- Separated pollGroups from polls (Store, Service, Mapper, ...)
2425
- Catch CronJob runs and report as error, but avoid crash at higher thread levels
2526
- Changed poll loading triggers (mainly navigation affected)
2627
- Added some status to the watchWorker
27-
- removed performance setting in favor of lazy loading participants
28+
- removed performance user setting in favor of lazy loading participants
29+
- reduce noise by avoiding toasts for obvious changes
2830

2931
### Fixes
3032
- Fixed broken endpoint for manually calling autoReminderCron
33+
- fix avatar foreground color
3134

3235
## [8.0.6] - 2025-07-03
3336
### Changes (8.0.6)

lib/Command/Share/Add.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6464
$emails = $input->getOption('email');
6565

6666
try {
67-
$poll = $this->pollMapper->find($pollId);
67+
$poll = $this->pollMapper->get($pollId);
6868
} catch (DoesNotExistException $e) {
6969
$output->writeln('<error>Poll not found.</error>');
7070
return 1;

lib/Command/Share/Remove.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6565
$emails = $input->getOption('email');
6666

6767
try {
68-
$poll = $this->pollMapper->find($pollId);
68+
$poll = $this->pollMapper->get($pollId);
6969
} catch (DoesNotExistException $e) {
7070
$output->writeln('<error>Poll not found.</error>');
7171
return 1;

lib/Controller/PollController.php

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,66 @@ public function get(int $pollId): JSONResponse {
102102
#[NoAdminRequired]
103103
#[FrontpageRoute(verb: 'GET', url: '/poll/{pollId}')]
104104
public function getFull(int $pollId): JSONResponse {
105-
return $this->response(fn () => [
106-
'poll' => $this->pollService->get($pollId),
107-
'options' => $this->optionService->list($pollId),
108-
'votes' => $this->voteService->list($pollId),
109-
'comments' => $this->commentService->list($pollId),
110-
'shares' => $this->shareService->list($pollId),
111-
'subscribed' => $this->subscriptionService->get($pollId),
112-
]);
105+
return $this->response(fn () => $this->getFullPoll($pollId), Http::STATUS_OK);
106+
107+
// return $this->response(fn () => [
108+
// 'poll' => $this->pollService->get($pollId),
109+
// 'options' => $this->optionService->list($pollId),
110+
// 'votes' => $this->voteService->list($pollId),
111+
// 'comments' => $this->commentService->list($pollId),
112+
// 'shares' => $this->shareService->list($pollId),
113+
// 'subscribed' => $this->subscriptionService->get($pollId),
114+
// ]);
115+
}
116+
117+
private function getFullPoll(int $pollId, bool $withTimings = false): array {
118+
$timerMicro['start'] = microtime(true);
119+
120+
$poll = $this->pollService->get($pollId);
121+
$timerMicro['poll'] = microtime(true);
122+
123+
$options = $this->optionService->list($pollId);
124+
$timerMicro['options'] = microtime(true);
125+
126+
$votes = $this->voteService->list($pollId);
127+
$timerMicro['votes'] = microtime(true);
128+
129+
$comments = $this->commentService->list($pollId);
130+
$timerMicro['comments'] = microtime(true);
131+
132+
$shares = $this->shareService->list($pollId);
133+
$timerMicro['shares'] = microtime(true);
134+
135+
$subscribed = $this->subscriptionService->get($pollId);
136+
$timerMicro['subscribed'] = microtime(true);
137+
138+
$diffMicro['total'] = microtime(true) - $timerMicro['start'];
139+
$diffMicro['poll'] = $timerMicro['poll'] - $timerMicro['start'];
140+
$diffMicro['options'] = $timerMicro['options'] - $timerMicro['poll'];
141+
$diffMicro['votes'] = $timerMicro['votes'] - $timerMicro['options'];
142+
$diffMicro['comments'] = $timerMicro['comments'] - $timerMicro['votes'];
143+
$diffMicro['shares'] = $timerMicro['shares'] - $timerMicro['comments'];
144+
$diffMicro['subscribed'] = $timerMicro['subscribed'] - $timerMicro['shares'];
145+
146+
if ($withTimings) {
147+
return [
148+
'poll' => $poll,
149+
'options' => $options,
150+
'votes' => $votes,
151+
'comments' => $comments,
152+
'shares' => $shares,
153+
'subscribed' => $subscribed,
154+
'diffMicro' => $diffMicro,
155+
];
156+
}
157+
return [
158+
'poll' => $poll,
159+
'options' => $options,
160+
'votes' => $votes,
161+
'comments' => $comments,
162+
'shares' => $shares,
163+
'subscribed' => $subscribed,
164+
];
113165
}
114166

115167
/**

lib/Db/Poll.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@
6666
* @method void setVotingVariant(string $value)
6767
*
6868
* Magic functions for joined columns
69-
* @method int getMinDate()
70-
* @method int getMaxDate()
7169
* @method int getShareToken()
7270
* @method int getOptionsCount()
7371
* @method int getProposalsCount()
@@ -168,8 +166,8 @@ class Poll extends EntityWithUser implements JsonSerializable {
168166

169167
// joined columns
170168
protected ?int $isCurrentUserLocked = 0;
171-
protected int $maxDate = 0;
172-
protected int $minDate = 0;
169+
protected ?int $maxDate = 0;
170+
protected ?int $minDate = 0;
173171
protected string $userRole = self::ROLE_NONE;
174172
protected string $shareToken = '';
175173
protected ?string $groupShares = '';
@@ -517,6 +515,19 @@ private function getDescriptionSafe(): string {
517515
return htmlspecialchars($this->getDescription());
518516
}
519517

518+
private function getMaxDate(): int {
519+
if ($this->maxDate === null) {
520+
return 0;
521+
}
522+
return $this->maxDate;
523+
}
524+
525+
private function getMinDate(): int {
526+
if ($this->minDate === null) {
527+
return time();
528+
}
529+
return $this->minDate;
530+
}
520531

521532
private function setMiscSettingsArray(array $value): void {
522533
$this->setMiscSettings(json_encode($value));

lib/Db/PollMapper.php

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,27 @@ public function __construct(
3535
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result
3636
* @return Poll
3737
*/
38-
public function get(int $id, bool $getDeleted = false): Poll {
38+
public function get(int $id, bool $getDeleted = false, bool $withRoles = false): Poll {
3939
$qb = $this->db->getQueryBuilder();
40-
$qb->select('*')
41-
->from($this->getTableName())
42-
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
40+
$qb->select(self::TABLE . '.*')
41+
->from($this->getTableName(), self::TABLE)
42+
->where($qb->expr()->eq(self::TABLE . '.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
43+
->groupBy(self::TABLE . '.id');
44+
4345
if (!$getDeleted) {
44-
$qb->andWhere($qb->expr()->eq('deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT)));
46+
$qb->andWhere($qb->expr()->eq(self::TABLE . '.deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT)));
47+
}
48+
49+
if ($withRoles) {
50+
$pollGroupsAlias = 'poll_groups';
51+
$currentUserId = $this->userSession->getCurrentUserId();
52+
// $this->joinOptions($qb, self::TABLE);
53+
$this->joinUserRole($qb, self::TABLE, $currentUserId);
54+
$this->joinGroupShares($qb, self::TABLE);
55+
$this->joinPollGroups($qb, self::TABLE, $pollGroupsAlias);
56+
$this->joinPollGroupShares($qb, $pollGroupsAlias, $currentUserId, $pollGroupsAlias);
57+
// $this->joinVotesCount($qb, self::TABLE, $currentUserId);
58+
// $this->joinParticipantsCount($qb, self::TABLE);
4559
}
4660
return $this->findEntity($qb);
4761
}
@@ -184,19 +198,19 @@ public function deleteByUserId(string $userId): void {
184198
* Build the enhanced query with joined tables
185199
*/
186200
protected function buildQuery(): IQueryBuilder {
187-
$currentUserId = $this->userSession->getCurrentUserId();
188201
$qb = $this->db->getQueryBuilder();
189202

190203
$qb->select(self::TABLE . '.*')
191204
->from($this->getTableName(), self::TABLE)
192205
->groupBy(self::TABLE . '.id');
193206

207+
$currentUserId = $this->userSession->getCurrentUserId();
194208
$pollGroupsAlias = 'poll_groups';
195209
$this->joinOptions($qb, self::TABLE);
196210
$this->joinUserRole($qb, self::TABLE, $currentUserId);
197211
$this->joinGroupShares($qb, self::TABLE);
198212
$this->joinPollGroups($qb, self::TABLE, $pollGroupsAlias);
199-
$this->joinUserSharesfromPollGroups($qb, $pollGroupsAlias, $currentUserId, $pollGroupsAlias);
213+
$this->joinPollGroupShares($qb, $pollGroupsAlias, $currentUserId, $pollGroupsAlias);
200214
$this->joinVotesCount($qb, self::TABLE, $currentUserId);
201215
$this->joinParticipantsCount($qb, self::TABLE);
202216
return $qb;
@@ -299,7 +313,7 @@ protected function joinPollGroups(
299313
* Supported share types are User and Admin
300314
* Groups, Teams will not work atm.
301315
*/
302-
protected function joinUserSharesfromPollGroups(
316+
protected function joinPollGroupShares(
303317
IQueryBuilder $qb,
304318
string $fromAlias,
305319
string $currentUserId,
@@ -338,13 +352,16 @@ protected function joinOptions(
338352
string $fromAlias,
339353
string $joinAlias = 'options',
340354
): void {
355+
// add highest option date
356+
$qb->addSelect($qb->createFunction('MAX(' . $joinAlias . '.timestamp) AS max_date'));
357+
358+
// add lowest option date
359+
$qb->addSelect($qb->createFunction('MIN(' . $joinAlias . '.timestamp) AS min_date'));
341360

342-
$zero = $qb->expr()->literal(0, IQueryBuilder::PARAM_INT);
343-
$saveMin = $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT);
361+
// add number of options with an owner (results in number of proposals)
362+
$qb->addSelect($qb->createFunction('COUNT(DISTINCT(CASE WHEN ' . $joinAlias . '.owner != \'\' THEN 1 END)) AS proposals_count'));
344363

345-
$qb->addSelect($qb->createFunction('coalesce(MAX(' . $joinAlias . '.timestamp), ' . $zero . ') AS max_date'))
346-
->addSelect($qb->createFunction('coalesce(MIN(' . $joinAlias . '.timestamp), ' . $saveMin . ') AS min_date'))
347-
->addSelect($qb->createFunction('COUNT(DISTINCT(CASE WHEN ' . $joinAlias . '.owner != \'\' THEN 1 END)) AS proposals_count'));
364+
// count number of options by counting unique ids
348365
$qb->selectAlias($qb->func()->count($joinAlias . '.id'), 'optionsCount');
349366

350367
$qb->leftJoin(

lib/Helper/Container.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static function getPoll(int $pollId, bool $getDeleted = false): Poll {
3838
}
3939

4040
public static function queryPoll(int $pollId): Poll {
41-
return Server::get(PollMapper::class)->find($pollId);
41+
return Server::get(PollMapper::class)->get($pollId);
4242
}
4343

4444
public static function findShare(int $pollId, string $userId): Share {

lib/Service/CommentService.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ public function __construct(
3636
*/
3737
public function list(int $pollId): array {
3838
try {
39-
$this->pollMapper->find($pollId)->request(Poll::PERMISSION_COMMENT_ADD);
39+
$this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_COMMENT_ADD);
4040
} catch (Exception $e) {
4141
return [];
4242
}
43-
$this->pollMapper->find($pollId)->request(Poll::PERMISSION_COMMENT_ADD);
43+
$this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_COMMENT_ADD);
4444

4545
$comments = $this->commentMapper->findByPoll($pollId);
4646
// treat comments from the same user within 5 minutes as grouped comments
@@ -66,7 +66,7 @@ public function list(int $pollId): array {
6666
* Add comment
6767
*/
6868
public function add(string $message, int $pollId, ?bool $confidential = false): Comment {
69-
$poll = $this->pollMapper->find($pollId);
69+
$poll = $this->pollMapper->get($pollId, withRoles: true);
7070
$poll->request(Poll::PERMISSION_COMMENT_ADD);
7171

7272
if ($poll->getForceConfidentialComments()) {
@@ -104,7 +104,7 @@ public function delete(int $commentId, bool $restore = false): Comment {
104104
$this->comment = $this->commentMapper->find($commentId);
105105

106106
if (!$this->comment->getCurrentUserIsEntityUser()) {
107-
$this->pollMapper->find($this->comment->getPollId())->request(Poll::PERMISSION_COMMENT_DELETE);
107+
$this->pollMapper->get($this->comment->getPollId(), withRoles: true)->request(Poll::PERMISSION_COMMENT_DELETE);
108108
}
109109

110110
$this->comment->setDeleted($restore ? 0 : time());

lib/Service/MailService.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ public function sendAutoReminder(): void {
227227
* Send a confirmation mail for the poll to all participants
228228
*/
229229
public function sendConfirmations(int $pollId): SentResult {
230-
$this->pollMapper->find($pollId)->request(Poll::PERMISSION_POLL_EDIT);
230+
$this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_POLL_EDIT);
231231
$sentResult = new SentResult();
232232

233233
$participants = $this->userMapper->getParticipants($pollId);

0 commit comments

Comments
 (0)