Skip to content

[2.x] perf: load the accepted policies of every loaded user together, whoever checks them - #91

Merged
imorland merged 2 commits into
2.xfrom
im/batch-policy-state-for-loaded-users
Oct 8, 2026
Merged

imorland merged 2 commits into
2.xfrom
im/batch-policy-state-for-loaded-users

Conversation

@imorland

@imorland imorland commented Oct 8, 2026

Copy link
Copy Markdown
Member

No issue: found from query profiling of discussion pages on discuss.flarum.org.

Changes proposed in this pull request:

Checking any user's permissions runs this extension's permission group processor, which needs that user's accepted policies. Other extensions check permissions for every user they serialize. On discuss, fof/ignore-users' ignored field does it for each user on the page.

#85 eager-loads the accepted policies for the users of a few endpoint relations (discussion authors, last posters, post authors). Users reached through any other relation still loaded their accepted policies one query at a time. On one discuss discussion page that was 19 queries for 16 users: the users who liked posts (flarum/likes likes) and the posts' editors (editedUser).

This makes the loading independent of how a user was reached:

  • LoadedUsers keeps track of the users loaded so far, through Eloquent's retrieved event, in a WeakMap, so tracking never keeps a user in memory, for example in a queue worker.
  • When a user's accepted policies are needed and aren't loaded, PolicyRepository::state() loads them for that user and every tracked user still without them, in one query (chunked at 1000 users, for the database's limit on bound parameters).
  • With no policies at all, a user's state is now empty without looking anything up. Before, every permission check still loaded the user's (empty) acceptance rows.

#85's eager loads and its PolicyStateBuffer are unchanged.

Reviewers should focus on:

  • The batch only covers users already loaded when the first one is needed. The actor's own check usually runs as the request starts, before anyone else is loaded, so that is a separate query; everyone loaded afterwards is batched together.
  • Unsaved users (guests) are skipped: they have nothing to load, and reading the relation doesn't query.
  • PolicyStateForLoadedUsersTest lists posts with their editedUser for six editors, one of whom hasn't accepted the required policy, while a test field checks startDiscussion on every serialized user, as other extensions do. It asserts the number of acceptance queries and that the editor who hasn't accepted is still restricted. Each user's groups lookup is exempted from the repeated-query check, with a comment: that's the cost of the extension doing the check.
  • The commits are in that order so CI shows the new tests before and after the change:
  • A test:integration:filter composer script runs single test classes through composer.

Screenshot

N/A

Confirmed

  • Frontend changes: tested on a local Flarum installation. (No frontend changes.)
  • Backend changes: tests are green (run composer test).

Required changes:

@imorland
imorland requested a review from a team as a code owner October 8, 2026 13:58
@imorland
imorland merged commit 15efe89 into 2.x Oct 8, 2026
25 checks passed
@imorland
imorland deleted the im/batch-policy-state-for-loaded-users branch October 8, 2026 14:00
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.

1 participant