mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 04:03:57 -06:00
FIX: Sidebar custom sections rendering perf degrades over time (#21552)
What is the problem? The main problem here is that we were incorrectly registering the same `onStateChange` callback with `TopicTrackingState` each time a user reads a post. When a user reads a post, the state in `TopicTrackingState` is updated and it triggers all the `onStateChange` callbacks which have been registered. In the `CommunitySection` class, we register a callback which would then call the `onTopicTrackingStateChange` method for each link in the class. For the `EverythingSectionLink` class, this would lookup the state in `TopicTrackingState` to get a new count of unread/new topics and update the `totalUnread` and `totalNew` properties which are tracked. For some reason that I have yet to figure out, updating the either of the tracked properties would result in Ember rerendering the entire `{{#each this.sections as |section|}}` in `component/sidebar/user/custom-sections.hbs` template. Note that `this.sections` refers to a `@cached` getter in the `SidebarUserCustomSections` class. The problem is that the `sections` getter is initializing a new bunch of sidebar sections related classes without calling the teardown function. As a result, we end up registering new `onStateChange` callbacks in `TopicTrackingState` in `CommunitySection` without removing the old ones. Over time, the number of callbacks build up and we end up slowing down the application. While we do not know the reason why defining a getter for the `sections` is causing the entire block to re-render, I realized that it is dangerous to use a getter for `sections` here since we have very little control on when the cached is broken. Instead, I moved the `sections` getter to a tracked property instead where the property is updated via `appEvents`. With this change, updating the tracked properties in `EverythingSectionLink` is no longer triggering a complete re-render of the said block above. We also now call `teardown` on the section objects that has been initialised before updating the `sections` property.
This commit is contained in:
parent
580f60d61d
commit
1106e4ad09
@ -2,9 +2,12 @@ import Component from "@glimmer/component";
|
|||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import { cached } from "@glimmer/tracking";
|
|
||||||
import Section from "discourse/lib/sidebar/section";
|
import Section from "discourse/lib/sidebar/section";
|
||||||
import CommunitySection from "discourse/lib/sidebar/community-section";
|
import CommunitySection from "discourse/lib/sidebar/community-section";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
|
export const REFRESH_CUSTOM_SIDEBAR_SECTIONS_APP_EVENT_NAME =
|
||||||
|
"sidebar:refresh-custom-sections";
|
||||||
|
|
||||||
export default class SidebarUserCustomSections extends Component {
|
export default class SidebarUserCustomSections extends Component {
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@ -15,21 +18,45 @@ export default class SidebarUserCustomSections extends Component {
|
|||||||
@service site;
|
@service site;
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
|
|
||||||
|
@tracked sections = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.messageBus.subscribe("/refresh-sidebar-sections", this._refresh);
|
this.messageBus.subscribe("/refresh-sidebar-sections", this._refresh);
|
||||||
|
|
||||||
|
this.appEvents.on(
|
||||||
|
REFRESH_CUSTOM_SIDEBAR_SECTIONS_APP_EVENT_NAME,
|
||||||
|
this,
|
||||||
|
this._refreshSections
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#initSections();
|
||||||
}
|
}
|
||||||
|
|
||||||
willDestroy() {
|
willDestroy() {
|
||||||
|
this.appEvents.off(
|
||||||
|
REFRESH_CUSTOM_SIDEBAR_SECTIONS_APP_EVENT_NAME,
|
||||||
|
this,
|
||||||
|
this._refreshSections
|
||||||
|
);
|
||||||
|
|
||||||
this.messageBus.unsubscribe("/refresh-sidebar-sections");
|
this.messageBus.unsubscribe("/refresh-sidebar-sections");
|
||||||
|
this.#teardown();
|
||||||
|
}
|
||||||
|
|
||||||
|
#teardown() {
|
||||||
return this.sections.forEach((section) => {
|
return this.sections.forEach((section) => {
|
||||||
section.teardown?.();
|
section.teardown?.();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@cached
|
_refreshSections() {
|
||||||
get sections() {
|
this.#teardown();
|
||||||
return this.currentUser.sidebarSections.map((section) => {
|
this.#initSections();
|
||||||
|
}
|
||||||
|
|
||||||
|
#initSections() {
|
||||||
|
this.sections = this.currentUser.sidebarSections.map((section) => {
|
||||||
switch (section.section_type) {
|
switch (section.section_type) {
|
||||||
case "community":
|
case "community":
|
||||||
const systemSection = new CommunitySection({
|
const systemSection = new CommunitySection({
|
||||||
@ -62,7 +89,7 @@ export default class SidebarUserCustomSections extends Component {
|
|||||||
@bind
|
@bind
|
||||||
_refresh() {
|
_refresh() {
|
||||||
return ajax("/sidebar_sections.json", {}).then((json) => {
|
return ajax("/sidebar_sections.json", {}).then((json) => {
|
||||||
this.currentUser.set("sidebar_sections", json.sidebar_sections);
|
this.currentUser.updateSidebarSections(json.sidebar_sections);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,8 +268,7 @@ export default Controller.extend(ModalFunctionality, {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.currentUser.set(
|
this.currentUser.updateSidebarSections(
|
||||||
"sidebar_sections",
|
|
||||||
this.currentUser.sidebar_sections.concat(data.sidebar_section)
|
this.currentUser.sidebar_sections.concat(data.sidebar_section)
|
||||||
);
|
);
|
||||||
this.send("closeModal");
|
this.send("closeModal");
|
||||||
@ -310,7 +309,7 @@ export default Controller.extend(ModalFunctionality, {
|
|||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.currentUser.set("sidebar_sections", newSidebarSections);
|
this.currentUser.updateSidebarSections(newSidebarSections);
|
||||||
this.send("closeModal");
|
this.send("closeModal");
|
||||||
})
|
})
|
||||||
.catch((e) =>
|
.catch((e) =>
|
||||||
@ -360,7 +359,7 @@ export default Controller.extend(ModalFunctionality, {
|
|||||||
this.currentUser.sidebar_sections.filter((section) => {
|
this.currentUser.sidebar_sections.filter((section) => {
|
||||||
return section.id !== data["sidebar_section"].id;
|
return section.id !== data["sidebar_section"].id;
|
||||||
});
|
});
|
||||||
this.currentUser.set("sidebar_sections", newSidebarSections);
|
this.currentUser.updateSidebarSections(newSidebarSections);
|
||||||
this.send("closeModal");
|
this.send("closeModal");
|
||||||
})
|
})
|
||||||
.catch((e) =>
|
.catch((e) =>
|
||||||
|
@ -95,6 +95,7 @@ export default class CommunitySection {
|
|||||||
if (this.callbackId) {
|
if (this.callbackId) {
|
||||||
this.topicTrackingState.offStateChange(this.callbackId);
|
this.topicTrackingState.offStateChange(this.callbackId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[...this.links, ...this.moreLinks].forEach((sectionLink) => {
|
[...this.links, ...this.moreLinks].forEach((sectionLink) => {
|
||||||
sectionLink.teardown?.();
|
sectionLink.teardown?.();
|
||||||
});
|
});
|
||||||
|
@ -12,22 +12,19 @@ export default class ReviewSectionLink extends BaseSectionLink {
|
|||||||
super(...arguments);
|
super(...arguments);
|
||||||
|
|
||||||
this._refreshCanDisplay();
|
this._refreshCanDisplay();
|
||||||
if (this.shouldDisplay) {
|
|
||||||
this.appEvents.on(
|
this.appEvents?.on(
|
||||||
"user-reviewable-count:changed",
|
"user-reviewable-count:changed",
|
||||||
this._refreshCanDisplay
|
this._refreshCanDisplay
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
teardown() {
|
teardown() {
|
||||||
if (this.shouldDisplay) {
|
this.appEvents?.off(
|
||||||
this.appEvents.off(
|
|
||||||
"user-reviewable-count:changed",
|
"user-reviewable-count:changed",
|
||||||
this._refreshCanDisplay
|
this._refreshCanDisplay
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
_refreshCanDisplay() {
|
_refreshCanDisplay() {
|
||||||
|
@ -51,6 +51,7 @@ import {
|
|||||||
showUserTip,
|
showUserTip,
|
||||||
} from "discourse/lib/user-tips";
|
} from "discourse/lib/user-tips";
|
||||||
import { dependentKeyCompat } from "@ember/object/compat";
|
import { dependentKeyCompat } from "@ember/object/compat";
|
||||||
|
import { REFRESH_CUSTOM_SIDEBAR_SECTIONS_APP_EVENT_NAME } from "discourse/components/sidebar/user/custom-sections";
|
||||||
|
|
||||||
export const SECOND_FACTOR_METHODS = {
|
export const SECOND_FACTOR_METHODS = {
|
||||||
TOTP: 1,
|
TOTP: 1,
|
||||||
@ -1161,6 +1162,11 @@ const User = RestModel.extend({
|
|||||||
this.appEvents.trigger("user-reviewable-count:changed", count);
|
this.appEvents.trigger("user-reviewable-count:changed", count);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateSidebarSections(sections) {
|
||||||
|
this.set("sidebar_sections", sections);
|
||||||
|
this.appEvents.trigger(REFRESH_CUSTOM_SIDEBAR_SECTIONS_APP_EVENT_NAME);
|
||||||
|
},
|
||||||
|
|
||||||
isInDoNotDisturb() {
|
isInDoNotDisturb() {
|
||||||
return (
|
return (
|
||||||
this.do_not_disturb_until &&
|
this.do_not_disturb_until &&
|
||||||
|
@ -791,6 +791,14 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
|
|||||||
"shows suffix indicator for unread posts on everything link"
|
"shows suffix indicator for unread posts on everything link"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const topicTrackingState = this.container.lookup(
|
||||||
|
"service:topic-tracking-state"
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialCallbackCount = Object.keys(
|
||||||
|
topicTrackingState.stateChangeCallbacks
|
||||||
|
).length;
|
||||||
|
|
||||||
// simulate reading topic 2
|
// simulate reading topic 2
|
||||||
await publishToMessageBus("/unread", {
|
await publishToMessageBus("/unread", {
|
||||||
topic_id: 2,
|
topic_id: 2,
|
||||||
@ -809,6 +817,12 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
|
|||||||
"shows suffix indicator for new topics on categories link"
|
"shows suffix indicator for new topics on categories link"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
Object.keys(topicTrackingState.stateChangeCallbacks).length,
|
||||||
|
initialCallbackCount,
|
||||||
|
"it does not add a new topic tracking state callback when the topic is read"
|
||||||
|
);
|
||||||
|
|
||||||
// simulate reading topic 1
|
// simulate reading topic 1
|
||||||
await publishToMessageBus("/unread", {
|
await publishToMessageBus("/unread", {
|
||||||
topic_id: 1,
|
topic_id: 1,
|
||||||
|
Loading…
Reference in New Issue
Block a user