Skip to content

Commit be5ad55

Browse files
committed
Caching main index views
This commit adds caching for the main index views (without filters) to speed up the "typical" use case where many users are viewing the latest topics. To still support logged in user / team features (icons/badges), all of these features were extracted into an eager loaded turbo frame, so that even if we generate the cache with a logged in user, we do not include any user specific details in the display.
1 parent 8c84bd5 commit be5ad55

14 files changed

+219
-84
lines changed

app/assets/stylesheets/components/topics.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@
193193
}
194194
}
195195

196+
.is-hidden {
197+
display: none !important;
198+
}
199+
196200
.topic-row {
197201
&:hover {
198202
background: var(--color-bg-hover);

app/controllers/topics_controller.rb

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ def index
88

99
apply_cursor_pagination(base_query)
1010
@new_topics_count = 0
11+
@page_cache_key = topics_page_cache_key
1112

12-
preload_topic_states if user_signed_in?
13-
preload_note_counts if user_signed_in?
1413
load_visible_tags if user_signed_in?
15-
preload_participation_flags if user_signed_in?
1614

1715
respond_to do |format|
1816
format.html
19-
format.turbo_stream
17+
format.turbo_stream do
18+
body = topics_turbo_stream_cache_fetch do
19+
render_to_string(:index, formats: [:turbo_stream])
20+
end
21+
render body:, content_type: "text/vnd.turbo-stream.html"
22+
end
2023
end
2124
end
2225

@@ -106,6 +109,61 @@ def search
106109
end
107110
end
108111

112+
def user_state
113+
topic_ids = params[:topic_ids].is_a?(Array) ? params[:topic_ids].map(&:to_i).uniq : []
114+
return render json: { topics: {} } unless user_signed_in? && topic_ids.any?
115+
116+
@topics = Topic.where(id: topic_ids)
117+
preload_topic_states
118+
preload_note_counts
119+
preload_participation_flags
120+
121+
payload = topic_ids.index_with do |tid|
122+
state = @topic_states[tid] || {}
123+
readers = Array(state[:team_readers]).map do |entry|
124+
{
125+
status: entry[:status],
126+
user_id: entry[:user]&.id,
127+
team_ids: entry[:team_ids]
128+
}
129+
end
130+
participation = @participation_flags&.dig(tid) || { mine: false, team: false, aliases: [] }
131+
participation_payload = {
132+
mine: participation[:mine],
133+
team: participation[:team],
134+
aliases_count: Array(participation[:aliases]).size
135+
}
136+
{
137+
status: state[:status],
138+
progress: state[:progress],
139+
read_count: state[:read_count],
140+
last_id: state[:last_id],
141+
aware_until: state[:aware_until],
142+
team_readers: readers,
143+
note_count: @topic_note_counts&.dig(tid).to_i,
144+
participation: participation_payload
145+
}
146+
end
147+
148+
render json: { topics: payload }
149+
end
150+
151+
def user_state_frame
152+
topic_ids = params[:topic_ids].is_a?(Array) ? params[:topic_ids].map(&:to_i).uniq : []
153+
return head :unauthorized unless user_signed_in?
154+
return head :ok if topic_ids.empty?
155+
156+
@topics = Topic.includes(:creator).where(id: topic_ids)
157+
preload_topic_states
158+
preload_note_counts
159+
preload_participation_flags
160+
161+
respond_to do |format|
162+
format.turbo_stream
163+
format.html { head :not_acceptable }
164+
end
165+
end
166+
109167
private
110168

111169
def set_topic
@@ -671,4 +729,32 @@ def hydrate_topics_from_entries(entries)
671729
topic
672730
end
673731
end
732+
733+
def topics_page_cache_key
734+
return nil unless @topics&.last
735+
return nil if params[:filter].present? || params[:team_id].present?
736+
737+
last_topic = @topics.last
738+
watermark = "#{last_topic.last_activity.to_i}_#{last_topic.id}"
739+
["topics-index", watermark]
740+
end
741+
742+
def topics_turbo_stream_cache_key
743+
[
744+
"topics-index-turbo",
745+
params[:filter],
746+
params[:team_id],
747+
params[:cursor].presence || "root"
748+
]
749+
end
750+
751+
def topics_turbo_stream_cache_read
752+
Rails.cache.read(topics_turbo_stream_cache_key)
753+
end
754+
755+
def topics_turbo_stream_cache_fetch
756+
Rails.cache.fetch(topics_turbo_stream_cache_key, expires_in: 10.minutes) do
757+
yield
758+
end
759+
end
674760
end

app/services/search_result_cache.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ def cache_key(watermarks)
5757
"wm", watermark_part,
5858
"lp", @longpage
5959
].join(":")
60-
File.open("cachelog.txt", 'a') do |f|
61-
f.puts "CACHE KEY: #{key}"
62-
end
6360
key
6461
end
6562

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- count = count.to_i
2+
- classes = ["topic-icon", "activity-note"]
3+
- classes << "is-hidden" unless count.positive?
4+
div class=classes.join(" ") id=dom_id(topic, "notes") data-controller="hover-popover" data-hover-popover-delay-value="200" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
5+
i.fa-solid.fa-note-sticky
6+
- if count.positive?
7+
span.topic-icon-badge = count
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
- classes = ["topic-icon", "activity-team"]
2+
- classes << "is-mine" if participation[:mine]
3+
- classes << "is-hidden" unless participation[:mine] || participation[:team]
4+
- aliases = participation[:aliases] || []
5+
- count = aliases.size
6+
div class=classes.join(" ") id=dom_id(topic, "participation") data-controller="hover-popover" data-hover-popover-delay-value="200" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
7+
i.fa-solid.fa-user-group
8+
- if count > 1
9+
span.topic-icon-badge = count
10+
- if aliases.any?
11+
.topic-icon-hover data-hover-popover-target="popover" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
12+
- aliases.each do |alias_record|
13+
- participant_stub = { alias: alias_record }
14+
= render partial: "participant_row", locals: { participant: participant_stub, avatar_size: 32, tooltip: alias_record.name }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
- status = state[:status] || "new"
2+
- status_class = "status-#{status}"
3+
td.topic-title.status-border class=status_class id=dom_id(topic, "status_cell") data-label="Topic"
4+
- if status.to_s == "reading"
5+
- read_count = state[:read_count].to_i
6+
- total_count = topic.messages.count
7+
- unread_count = [total_count - read_count, 0].max
8+
.topic-icon.topic-icon-reading
9+
i.fa-solid.fa-envelope
10+
- if unread_count.positive?
11+
span.topic-icon-badge.topic-icon-badge-sup = unread_count
12+
= render partial: "topics/note_icon", locals: { topic: topic, count: note_count.to_i }
13+
= render partial: "topics/team_readers_icon", locals: { topic: topic, readers: team_readers }
14+
- if topic.attachments.exists?
15+
.topic-icon
16+
i.fa-solid.fa-paperclip
17+
= link_to topic.title, topic_path(topic), class: "topic-link"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
- count = readers&.size.to_i
2+
- classes = ["topic-icon", "activity-team-read"]
3+
- classes << "is-hidden" if count.zero?
4+
div class=classes.join(" ") id=dom_id(topic, "team_readers") data-controller="hover-popover" data-hover-popover-delay-value="200" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
5+
i.fa-solid.fa-users
6+
- if count.positive?
7+
span.topic-icon-badge = count
8+
.topic-icon-hover data-hover-popover-target="popover" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
9+
- readers.each do |reader|
10+
- alias_record = reader[:user]&.primary_alias || reader[:user]&.aliases&.first
11+
- next unless alias_record
12+
- participant_stub = { alias: alias_record }
13+
= render partial: "participant_row", locals: { participant: participant_stub, avatar_size: 32, tooltip: "#{alias_record.name} (#{reader[:status]})" }

app/views/topics/_topics.html.slim

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,6 @@
11
- topics.each do |topic|
2-
- state = @topic_states&.dig(topic.id) || {}
3-
- team_readers = state[:team_readers] || []
4-
- filtered_readers = team_readers
5-
- if params[:team_id].present?
6-
- filtered_readers = team_readers.select { |r| r[:team_ids]&.include?(params[:team_id].to_i) }
7-
- other_team_readers = filtered_readers.reject { |r| r[:user].id == current_user&.id }.uniq { |r| r[:user].id }
8-
tr class="topic-row topic-#{state[:status]}" data-topic-id=topic.id data-last-message-id=topic.messages.maximum(:id)
9-
td class="topic-title status-border status-#{state[:status] || 'new'}" data-label="Topic"
10-
- if state[:status] == :reading
11-
- unread_count = [topic.messages.count - state[:read_count], 0].max
12-
.topic-icon.topic-icon-reading
13-
i.fa-solid.fa-envelope
14-
- if unread_count.positive?
15-
span.topic-icon-badge.topic-icon-badge-sup = unread_count
16-
- if user_signed_in? && other_team_readers.any?
17-
- reader_count = other_team_readers.size
18-
div class="topic-icon activity-team-read" data-controller="hover-popover" data-hover-popover-delay-value="200" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
19-
i.fa-solid.fa-users
20-
- if reader_count.positive?
21-
span.topic-icon-badge = reader_count
22-
.topic-icon-hover data-hover-popover-target="popover" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
23-
- other_team_readers.each do |reader|
24-
- alias_record = reader[:user].primary_alias || reader[:user].aliases.first
25-
- next unless alias_record
26-
- participant_stub = { alias: alias_record }
27-
= render partial: "participant_row", locals: { participant: participant_stub, avatar_size: 32, tooltip: "#{alias_record.name} (#{reader[:status]})" }
28-
- if user_signed_in?
29-
- note_count = @topic_note_counts&.dig(topic.id).to_i
30-
- if note_count.positive?
31-
div class="topic-icon activity-note" data-controller="hover-popover" data-hover-popover-delay-value="200" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
32-
i.fa-solid.fa-note-sticky
33-
span.topic-icon-badge = note_count
34-
- if topic.attachments.exists?
35-
.topic-icon
36-
i.fa-solid.fa-paperclip
37-
= link_to topic.title, topic_path(topic), class: "topic-link"
2+
tr class="topic-row" data-topic-id=topic.id data-last-message-id=topic.messages.maximum(:id)
3+
= render partial: "topics/status_cell", locals: { topic: topic, state: {}, note_count: 0, team_readers: [] }
384
td.topic-activity data-label="Activity"
395
- last_message = topic.messages.order(:created_at).last
406
- replies_count = [topic.messages.count - 1, 0].max
@@ -56,19 +22,4 @@
5622
.topic-icon-hover data-hover-popover-target="popover" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
5723
- contributor_participants.each do |participant|
5824
= render partial: "participant_row", locals: { participant: participant, avatar_size: 32 }
59-
- if user_signed_in?
60-
- participation = @participation_flags&.dig(topic.id)
61-
- show_team_icon = participation && (participation[:team] || participation[:mine])
62-
- if show_team_icon
63-
- icon_classes = ["topic-icon", "activity-team"]
64-
- icon_classes << "is-mine" if participation[:mine]
65-
- aliases = participation[:aliases] || []
66-
div class=icon_classes.join(" ") data-controller="hover-popover" data-hover-popover-delay-value="200" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
67-
i.fa-solid.fa-user-group
68-
- if aliases.size > 1
69-
span.topic-icon-badge = aliases.size
70-
- if aliases.any?
71-
.topic-icon-hover data-hover-popover-target="popover" data-action="mouseenter->hover-popover#show mouseleave->hover-popover#scheduleHide"
72-
- aliases.each do |alias_record|
73-
- participant_stub = { alias: alias_record }
74-
= render partial: "participant_row", locals: { participant: participant_stub, avatar_size: 32, tooltip: alias_record.name }
25+
= render partial: "topics/participation_icon", locals: { topic: topic, participation: {} }

app/views/topics/index.html.slim

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,45 @@
11
- content_for :title, "PostgreSQL Hackers Archive"
22

3-
= render partial: "sidebar", locals: { available_note_tags: @available_note_tags, search_query: @search_query }
3+
- cache_block = lambda do
4+
#new-topics-banner data-controller="new-topics-banner" data-new-topics-banner-url-value=new_topics_count_topics_path(viewing_since: @viewing_since.iso8601, filter: params[:filter], team_id: params[:team_id]) data-new-topics-banner-interval-ms-value="180000"
5+
= render partial: "new_topics_banner", locals: { count: @new_topics_count, viewing_since: @viewing_since }
46

5-
#new-topics-banner data-controller="new-topics-banner" data-new-topics-banner-url-value=new_topics_count_topics_path(viewing_since: @viewing_since.iso8601, filter: params[:filter], team_id: params[:team_id]) data-new-topics-banner-interval-ms-value="180000"
6-
= render partial: "new_topics_banner", locals: { count: @new_topics_count, viewing_since: @viewing_since }
7+
- if @active_note_tag.present?
8+
.tag-filter-banner
9+
span Filtering by ##{@active_note_tag}
10+
= link_to "Clear", topics_path, class: "clear-tag-filter"
711

8-
- if @active_note_tag.present?
9-
.tag-filter-banner
10-
span Filtering by ##{@active_note_tag}
11-
= link_to "Clear", topics_path, class: "clear-tag-filter"
12-
13-
.topics-table
1412
- if user_signed_in?
15-
.topics-actions data-controller="topics-aware" data-topics-aware-aware-url-value=aware_bulk_topics_path data-topics-aware-aware-all-url-value=aware_all_topics_path
16-
button.mark-aware-button data-action="click->topics-aware#markVisibleAware" Mark displayed threads aware
17-
button.mark-aware-button.secondary data-action="click->topics-aware#markAllAware" Mark everything up to now aware
13+
#user-state-requests
14+
= turbo_frame_tag "user-state-root", src: user_state_frame_topics_path(topic_ids: @topics.map(&:id), format: :turbo_stream), loading: :eager
15+
16+
.topics-table
17+
- if user_signed_in?
18+
.topics-actions data-controller="topics-aware" data-topics-aware-aware-url-value=aware_bulk_topics_path data-topics-aware-aware-all-url-value=aware_all_topics_path
19+
button.mark-aware-button data-action="click->topics-aware#markVisibleAware" Mark displayed threads aware
20+
button.mark-aware-button.secondary data-action="click->topics-aware#markAllAware" Mark everything up to now aware
21+
22+
table
23+
thead
24+
tr
25+
th.topic-title Topic
26+
th.activity-header Activity
27+
th.participants-header Participants
28+
tbody#topics
29+
= render partial: "topics", locals: { topics: @topics }
1830

19-
table
20-
thead
21-
tr
22-
th.topic-title Topic
23-
th.activity-header Activity
24-
th.participants-header Participants
25-
tbody#topics
26-
= render partial: "topics", locals: { topics: @topics }
31+
- if @topics.size == 25
32+
- last_topic = @topics.last
33+
- cursor = "#{last_topic.last_activity.iso8601}_#{last_topic.id}"
34+
= turbo_frame_tag "pagination", src: topics_path(cursor: cursor, viewing_since: @viewing_since.iso8601, format: :turbo_stream), loading: :lazy do
35+
.loading-indicator Loading more topics...
36+
- else
37+
= turbo_frame_tag "pagination"
2738

28-
- if @topics.size == 25
29-
- last_topic = @topics.last
30-
- cursor = "#{last_topic.last_activity.iso8601}_#{last_topic.id}"
31-
= turbo_frame_tag "pagination", src: topics_path(cursor: cursor, viewing_since: @viewing_since.iso8601, format: :turbo_stream), loading: :lazy do
32-
.loading-indicator Loading more topics...
39+
- if @page_cache_key
40+
= render partial: "sidebar", locals: { available_note_tags: @available_note_tags, search_query: @search_query }
41+
- cache(@page_cache_key) do
42+
- cache_block.call
43+
- else
44+
= render partial: "sidebar", locals: { available_note_tags: @available_note_tags, search_query: @search_query }
45+
- cache_block.call
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
= turbo_stream.append "topics" do
22
= render partial: "topics", locals: { topics: @topics }
33

4+
- if user_signed_in?
5+
- frame_id = params[:cursor].present? ? "user-state-#{params[:cursor]}" : "user-state-root"
6+
= turbo_stream.append "user-state-requests" do
7+
= turbo_frame_tag frame_id, src: user_state_frame_topics_path(topic_ids: @topics.map(&:id), format: :turbo_stream), loading: :eager
8+
49
- if @topics.size == 25
510
- last_topic = @topics.last
611
- cursor = "#{last_topic.last_activity.iso8601}_#{last_topic.id}"
712
= turbo_stream.replace "pagination" do
813
= turbo_frame_tag "pagination", src: topics_path(cursor: cursor, viewing_since: @viewing_since.iso8601, format: :turbo_stream), loading: :lazy do
914
.loading-indicator Loading more topics...
15+
- elsif params[:cursor].present?
16+
= turbo_stream.remove "pagination"

0 commit comments

Comments
 (0)