mirror of
https://github.com/discourse/discourse.git
synced 2025-05-23 12:11:12 +08:00
PERF: Move mention lookups out of the V8 context. (#6640)
We were looking up each mention one by one without any form of caching and that results in a problem somewhat similar to an N+1. When we have to do alot of DB lookups, it also increased the time spent in the V8 context which may eventually lead to a timeout. The change here makes it such that mention lookups only does a single DB query per post that happens outside of the V8 context.
This commit is contained in:
@ -1,32 +1,12 @@
|
|||||||
function addMention(buffer, matches, state) {
|
function addMention(buffer, matches, state) {
|
||||||
let username = matches[1] || matches[2];
|
let username = matches[1] || matches[2];
|
||||||
let { getURL, mentionLookup, formatUsername } = state.md.options.discourse;
|
let tag = "span";
|
||||||
|
|
||||||
let type = mentionLookup && mentionLookup(username);
|
|
||||||
|
|
||||||
let tag = "a";
|
|
||||||
let className = "mention";
|
let className = "mention";
|
||||||
let href = null;
|
|
||||||
|
|
||||||
if (type === "user") {
|
|
||||||
href = getURL("/u/") + username.toLowerCase();
|
|
||||||
} else if (type === "group") {
|
|
||||||
href = getURL("/groups/") + username;
|
|
||||||
className = "mention-group";
|
|
||||||
} else {
|
|
||||||
tag = "span";
|
|
||||||
}
|
|
||||||
|
|
||||||
let token = new state.Token("mention_open", tag, 1);
|
let token = new state.Token("mention_open", tag, 1);
|
||||||
token.attrs = [["class", className]];
|
token.attrs = [["class", className]];
|
||||||
if (href) {
|
|
||||||
token.attrs.push(["href", href]);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.push(token);
|
buffer.push(token);
|
||||||
if (formatUsername) {
|
|
||||||
username = formatUsername(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
token = new state.Token("text", "", 0);
|
token = new state.Token("text", "", 0);
|
||||||
token.content = "@" + username;
|
token.content = "@" + username;
|
||||||
|
@ -31,7 +31,6 @@ export function buildOptions(state) {
|
|||||||
previewing,
|
previewing,
|
||||||
linkify,
|
linkify,
|
||||||
censoredWords,
|
censoredWords,
|
||||||
mentionLookup,
|
|
||||||
invalidateOneboxes
|
invalidateOneboxes
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
@ -67,7 +66,6 @@ export function buildOptions(state) {
|
|||||||
lookupAvatarByPostNumber,
|
lookupAvatarByPostNumber,
|
||||||
lookupPrimaryUserGroupByPostNumber,
|
lookupPrimaryUserGroupByPostNumber,
|
||||||
formatUsername,
|
formatUsername,
|
||||||
mentionLookup,
|
|
||||||
emojiUnicodeReplacer,
|
emojiUnicodeReplacer,
|
||||||
lookupInlineOnebox,
|
lookupInlineOnebox,
|
||||||
lookupImageUrls,
|
lookupImageUrls,
|
||||||
|
@ -156,7 +156,6 @@ module PrettyText
|
|||||||
__optInput.formatUsername = __formatUsername;
|
__optInput.formatUsername = __formatUsername;
|
||||||
__optInput.getTopicInfo = __getTopicInfo;
|
__optInput.getTopicInfo = __getTopicInfo;
|
||||||
__optInput.categoryHashtagLookup = __categoryLookup;
|
__optInput.categoryHashtagLookup = __categoryLookup;
|
||||||
__optInput.mentionLookup = __mentionLookup;
|
|
||||||
__optInput.customEmoji = #{custom_emoji.to_json};
|
__optInput.customEmoji = #{custom_emoji.to_json};
|
||||||
__optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer;
|
__optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer;
|
||||||
__optInput.lookupInlineOnebox = __lookupInlineOnebox;
|
__optInput.lookupInlineOnebox = __lookupInlineOnebox;
|
||||||
@ -265,6 +264,8 @@ module PrettyText
|
|||||||
add_s3_cdn(doc)
|
add_s3_cdn(doc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
add_mentions(doc) if SiteSetting.enable_mentions
|
||||||
|
|
||||||
doc.to_html
|
doc.to_html
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -419,4 +420,67 @@ module PrettyText
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
USER_TYPE ||= 'user'
|
||||||
|
GROUP_TYPE ||= 'group'
|
||||||
|
|
||||||
|
def self.add_mentions(doc)
|
||||||
|
elements = doc.css("span.mention")
|
||||||
|
names = elements.map { |element| element.text[1..-1] }
|
||||||
|
|
||||||
|
mentions = lookup_mentions(names)
|
||||||
|
|
||||||
|
doc.css("span.mention").each do |element|
|
||||||
|
name = element.text[1..-1]
|
||||||
|
name.downcase!
|
||||||
|
|
||||||
|
if type = mentions[name]
|
||||||
|
element.name = 'a'
|
||||||
|
|
||||||
|
element.children = PrettyText::Helpers.format_username(
|
||||||
|
element.children.text
|
||||||
|
)
|
||||||
|
|
||||||
|
case type
|
||||||
|
when USER_TYPE
|
||||||
|
element['href'] = "/u/#{name}"
|
||||||
|
when GROUP_TYPE
|
||||||
|
element['class'] = 'mention-group'
|
||||||
|
element['href'] = "/groups/#{name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.lookup_mentions(names)
|
||||||
|
sql = <<~SQL
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
:user_type AS type,
|
||||||
|
username_lower AS name
|
||||||
|
FROM users
|
||||||
|
WHERE username_lower IN (:names)
|
||||||
|
)
|
||||||
|
UNION
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
:group_type AS type,
|
||||||
|
name
|
||||||
|
FROM groups
|
||||||
|
WHERE name IN (:names)
|
||||||
|
)
|
||||||
|
SQL
|
||||||
|
|
||||||
|
results = DB.query(sql,
|
||||||
|
names: names,
|
||||||
|
user_type: USER_TYPE,
|
||||||
|
group_type: GROUP_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
mentions = {}
|
||||||
|
results.each { |result| mentions[result.name] = result.type }
|
||||||
|
mentions
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -39,12 +39,6 @@ module PrettyText
|
|||||||
username
|
username
|
||||||
end
|
end
|
||||||
|
|
||||||
def mention_lookup(name)
|
|
||||||
return false if name.blank?
|
|
||||||
return "user" if User.exists?(username_lower: name.downcase)
|
|
||||||
return "group" if Group.exists?(name: name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def category_hashtag_lookup(category_slug)
|
def category_hashtag_lookup(category_slug)
|
||||||
if category = Category.query_from_hashtag_slug(category_slug)
|
if category = Category.query_from_hashtag_slug(category_slug)
|
||||||
[category.url_with_id, category_slug]
|
[category.url_with_id, category_slug]
|
||||||
|
@ -1,20 +1,26 @@
|
|||||||
__PrettyText = require('pretty-text/pretty-text').default;
|
__PrettyText = require("pretty-text/pretty-text").default;
|
||||||
__buildOptions = require('pretty-text/pretty-text').buildOptions;
|
__buildOptions = require("pretty-text/pretty-text").buildOptions;
|
||||||
__performEmojiUnescape = require('pretty-text/emoji').performEmojiUnescape;
|
__performEmojiUnescape = require("pretty-text/emoji").performEmojiUnescape;
|
||||||
|
|
||||||
__utils = require('discourse/lib/utilities');
|
__utils = require("discourse/lib/utilities");
|
||||||
|
|
||||||
__emojiUnicodeReplacer = null;
|
__emojiUnicodeReplacer = null;
|
||||||
|
|
||||||
__setUnicode = function(replacements) {
|
__setUnicode = function(replacements) {
|
||||||
let unicodeRegexp = new RegExp(Object.keys(replacements).sort().reverse().join("|"), "g");
|
let unicodeRegexp = new RegExp(
|
||||||
|
Object.keys(replacements)
|
||||||
|
.sort()
|
||||||
|
.reverse()
|
||||||
|
.join("|"),
|
||||||
|
"g"
|
||||||
|
);
|
||||||
|
|
||||||
__emojiUnicodeReplacer = function(text) {
|
__emojiUnicodeReplacer = function(text) {
|
||||||
unicodeRegexp.lastIndex = 0;
|
unicodeRegexp.lastIndex = 0;
|
||||||
let m;
|
let m;
|
||||||
while ((m = unicodeRegexp.exec(text)) !== null) {
|
while ((m = unicodeRegexp.exec(text)) !== null) {
|
||||||
let replacement = ":" + replacements[m[0]] + ":";
|
let replacement = ":" + replacements[m[0]] + ":";
|
||||||
const before = text.charAt(m.index-1);
|
const before = text.charAt(m.index - 1);
|
||||||
if (!/\B/.test(before)) {
|
if (!/\B/.test(before)) {
|
||||||
replacement = "\u200b" + replacement;
|
replacement = "\u200b" + replacement;
|
||||||
}
|
}
|
||||||
@ -23,7 +29,7 @@ __setUnicode = function(replacements) {
|
|||||||
|
|
||||||
// fixes Safari VARIATION SELECTOR-16 issue with some emojis
|
// fixes Safari VARIATION SELECTOR-16 issue with some emojis
|
||||||
// https://meta.discourse.org/t/emojis-selected-on-ios-displaying-additional-rectangles/86132
|
// https://meta.discourse.org/t/emojis-selected-on-ios-displaying-additional-rectangles/86132
|
||||||
text = text.replace(/\ufe0f/g, '');
|
text = text.replace(/\ufe0f/g, "");
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
@ -35,9 +41,13 @@ function __getURLNoCDN(url) {
|
|||||||
if (!url) return url;
|
if (!url) return url;
|
||||||
|
|
||||||
// if it's a non relative URL, return it.
|
// if it's a non relative URL, return it.
|
||||||
if (url !== '/' && !/^\/[^\/]/.test(url)) { return url; }
|
if (url !== "/" && !/^\/[^\/]/.test(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
if (url.indexOf(__paths.baseUri) !== -1) { return url; }
|
if (url.indexOf(__paths.baseUri) !== -1) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
if (url[0] !== "/") url = "/" + url;
|
if (url[0] !== "/") url = "/" + url;
|
||||||
|
|
||||||
return __paths.baseUri + url;
|
return __paths.baseUri + url;
|
||||||
@ -76,12 +86,11 @@ function __categoryLookup(c) {
|
|||||||
return __helpers.category_tag_hashtag_lookup(c);
|
return __helpers.category_tag_hashtag_lookup(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __mentionLookup(u) {
|
|
||||||
return __helpers.mention_lookup(u);
|
|
||||||
}
|
|
||||||
|
|
||||||
function __lookupAvatar(p) {
|
function __lookupAvatar(p) {
|
||||||
return __utils.avatarImg({size: "tiny", avatarTemplate: __helpers.avatar_template(p) }, __getURL);
|
return __utils.avatarImg(
|
||||||
|
{ size: "tiny", avatarTemplate: __helpers.avatar_template(p) },
|
||||||
|
__getURL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __formatUsername(username) {
|
function __formatUsername(username) {
|
||||||
@ -97,5 +106,7 @@ function __getCurrentUser(userId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
I18n = {
|
I18n = {
|
||||||
t: function(a,b) { return __helpers.t(a,b); }
|
t: function(a, b) {
|
||||||
|
return __helpers.t(a, b);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@ -220,18 +220,37 @@ describe PrettyText do
|
|||||||
expect(PrettyText.cook("hi\n@.s.s")).to eq("<p>hi<br>\n@.s.s</p>")
|
expect(PrettyText.cook("hi\n@.s.s")).to eq("<p>hi<br>\n@.s.s</p>")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can handle mention with hyperlinks" do
|
it "handles user and group mentions correctly" do
|
||||||
Fabricate(:user, username: "sam")
|
['user', 'user2'].each do |username |
|
||||||
expect(PrettyText.cook("hi @sam! hi")).to match_html '<p>hi <a class="mention" href="/u/sam">@sam</a>! hi</p>'
|
Fabricate(:user, username: username)
|
||||||
expect(PrettyText.cook("hi\n@sam.")).to eq("<p>hi<br>\n<a class=\"mention\" href=\"/u/sam\">@sam</a>.</p>")
|
end
|
||||||
end
|
|
||||||
|
|
||||||
it "can handle group mention" do
|
|
||||||
group = Fabricate(:group)
|
group = Fabricate(:group)
|
||||||
|
|
||||||
expect(PrettyText.cook("hi @#{group.name}! hi")).to match_html(
|
[
|
||||||
%Q{<p>hi <a class="mention-group" href="/groups/#{group.name}">@#{group.name}</a>! hi</p>}
|
[
|
||||||
)
|
'hi @user! @user2 hi',
|
||||||
|
'<p>hi <a class="mention" href="/u/user">@user</a>! <a class="mention" href="/u/user2">@user2</a> hi</p>'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"hi\n@user. @#{group.name} @somemention",
|
||||||
|
%Q|<p>hi<br>\n<a class="mention" href="/u/user">@user</a>. <a class="mention-group" href="/groups/#{group.name}">@#{group.name}</a> <span class="mention">@somemention</span></p>|
|
||||||
|
]
|
||||||
|
].each do |input, expected|
|
||||||
|
expect(PrettyText.cook(input)).to eq(expected)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'when mentions are disabled' do
|
||||||
|
before do
|
||||||
|
SiteSetting.enable_mentions = false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not convert mentions to links' do
|
||||||
|
user = Fabricate(:user)
|
||||||
|
|
||||||
|
expect(PrettyText.cook('hi @user')).to eq('<p>hi @user</p>')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can handle mentions inside a hyperlink" do
|
it "can handle mentions inside a hyperlink" do
|
||||||
|
@ -435,16 +435,9 @@ QUnit.test("Quotes", assert => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("Mentions", assert => {
|
QUnit.test("Mentions", assert => {
|
||||||
const alwaysTrue = {
|
assert.cooked(
|
||||||
mentionLookup: function() {
|
|
||||||
return "user";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
assert.cookedOptions(
|
|
||||||
"Hello @sam",
|
"Hello @sam",
|
||||||
alwaysTrue,
|
'<p>Hello <span class="mention">@sam</span></p>',
|
||||||
'<p>Hello <a class="mention" href="/u/sam">@sam</a></p>',
|
|
||||||
"translates mentions to links"
|
"translates mentions to links"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -454,9 +447,8 @@ QUnit.test("Mentions", assert => {
|
|||||||
"it doesn't do mentions within links"
|
"it doesn't do mentions within links"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.cookedOptions(
|
assert.cooked(
|
||||||
"[@codinghorror](https://twitter.com/codinghorror)",
|
"[@codinghorror](https://twitter.com/codinghorror)",
|
||||||
alwaysTrue,
|
|
||||||
'<p><a href="https://twitter.com/codinghorror">@codinghorror</a></p>',
|
'<p><a href="https://twitter.com/codinghorror">@codinghorror</a></p>',
|
||||||
"it doesn't do link mentions within links"
|
"it doesn't do link mentions within links"
|
||||||
);
|
);
|
||||||
@ -557,17 +549,9 @@ QUnit.test("Mentions", assert => {
|
|||||||
"handles mentions separated by a slash."
|
"handles mentions separated by a slash."
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.cookedOptions(
|
assert.cooked(
|
||||||
"@eviltrout",
|
|
||||||
alwaysTrue,
|
|
||||||
'<p><a class="mention" href="/u/eviltrout">@eviltrout</a></p>',
|
|
||||||
"it doesn't onebox mentions"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.cookedOptions(
|
|
||||||
"<small>a @sam c</small>",
|
"<small>a @sam c</small>",
|
||||||
alwaysTrue,
|
'<p><small>a <span class="mention">@sam</span> c</small></p>',
|
||||||
'<p><small>a <a class="mention" href="/u/sam">@sam</a> c</small></p>',
|
|
||||||
"it allows mentions within HTML tags"
|
"it allows mentions within HTML tags"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user