perf: slightly more efficient word filter format (#1991)
This commit is contained in:
parent
4adc8ff748
commit
5e61a8582b
|
@ -1,4 +1,5 @@
|
||||||
import { createRegexFromFilter } from '../../_utils/createRegexFromFilter'
|
import { createRegexFromFilters } from '../../_utils/createRegexFromFilters'
|
||||||
|
import { WORD_FILTER_CONTEXTS } from '../../_static/wordFilters'
|
||||||
|
|
||||||
export function wordFilterComputations (store) {
|
export function wordFilterComputations (store) {
|
||||||
// unexpiredInstanceFilters is calculated based on `now` and `instanceFilters`,
|
// unexpiredInstanceFilters is calculated based on `now` and `instanceFilters`,
|
||||||
|
@ -9,13 +10,17 @@ export function wordFilterComputations (store) {
|
||||||
(unexpiredInstanceFilters, currentInstance) => unexpiredInstanceFilters[currentInstance] || []
|
(unexpiredInstanceFilters, currentInstance) => unexpiredInstanceFilters[currentInstance] || []
|
||||||
)
|
)
|
||||||
|
|
||||||
store.compute('unexpiredInstanceFiltersWithRegexes', ['unexpiredInstanceFilters'], unexpiredInstanceFilters => {
|
store.compute('unexpiredInstanceFilterRegexes', ['unexpiredInstanceFilters'], unexpiredInstanceFilters => {
|
||||||
return Object.fromEntries(Object.entries(unexpiredInstanceFilters).map(([instanceName, filters]) => {
|
return Object.fromEntries(Object.entries(unexpiredInstanceFilters).map(([instanceName, filters]) => {
|
||||||
const filtersWithRegexes = filters.map(filter => ({
|
const contextsToRegex = Object.fromEntries(WORD_FILTER_CONTEXTS.map(context => {
|
||||||
...filter,
|
const filtersForThisContext = filters.filter(_ => _.context.includes(context))
|
||||||
regex: createRegexFromFilter(filter)
|
if (!filtersForThisContext.length) {
|
||||||
}))
|
return undefined // don't bother even adding it to the map
|
||||||
return [instanceName, filtersWithRegexes]
|
}
|
||||||
|
const regex = createRegexFromFilters(filtersForThisContext)
|
||||||
|
return [context, regex]
|
||||||
|
}).filter(Boolean))
|
||||||
|
return [instanceName, contextsToRegex]
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,8 @@ export function wordFilterObservers () {
|
||||||
updateUnexpiredInstanceFiltersIfUnchanged(now, instanceFilters)
|
updateUnexpiredInstanceFiltersIfUnchanged(now, instanceFilters)
|
||||||
})
|
})
|
||||||
|
|
||||||
store.observe('unexpiredInstanceFiltersWithRegexes', async unexpiredInstanceFiltersWithRegexes => {
|
store.observe('unexpiredInstanceFilterRegexes', async unexpiredInstanceFilterRegexes => {
|
||||||
console.log('unexpiredInstanceFiltersWithRegexes changed, recomputing filterContexts')
|
console.log('unexpiredInstanceFilterRegexes changed, recomputing filterContexts')
|
||||||
mark('update timeline item summary filter contexts')
|
mark('update timeline item summary filter contexts')
|
||||||
// Whenever the filters change, we need to re-compute the filterContexts on the TimelineSummaries.
|
// Whenever the filters change, we need to re-compute the filterContexts on the TimelineSummaries.
|
||||||
// This is a bit of an odd design, but we do it for perf. See timelineItemToSummary.js for details.
|
// This is a bit of an odd design, but we do it for perf. See timelineItemToSummary.js for details.
|
||||||
|
@ -55,7 +55,7 @@ export function wordFilterObservers () {
|
||||||
|
|
||||||
let somethingChanged = false
|
let somethingChanged = false
|
||||||
|
|
||||||
await Promise.all(Object.entries(unexpiredInstanceFiltersWithRegexes).map(async ([instanceName, filtersWithRegexes]) => {
|
await Promise.all(Object.entries(unexpiredInstanceFilterRegexes).map(async ([instanceName, contextsToRegex]) => {
|
||||||
const timelinesToSummaries = timelineItemSummaries[instanceName] || {}
|
const timelinesToSummaries = timelineItemSummaries[instanceName] || {}
|
||||||
const timelinesToSummariesToAdd = timelineItemSummariesToAdd[instanceName] || {}
|
const timelinesToSummariesToAdd = timelineItemSummariesToAdd[instanceName] || {}
|
||||||
const summariesToUpdate = [
|
const summariesToUpdate = [
|
||||||
|
@ -70,7 +70,7 @@ export function wordFilterObservers () {
|
||||||
? database.getNotification(instanceName, summary.id)
|
? database.getNotification(instanceName, summary.id)
|
||||||
: database.getStatus(instanceName, summary.id)
|
: database.getStatus(instanceName, summary.id)
|
||||||
)
|
)
|
||||||
const newFilterContexts = computeFilterContextsForStatusOrNotification(item, filtersWithRegexes)
|
const newFilterContexts = computeFilterContextsForStatusOrNotification(item, contextsToRegex)
|
||||||
if (!isEqual(summary.filterContexts, newFilterContexts)) {
|
if (!isEqual(summary.filterContexts, newFilterContexts)) {
|
||||||
somethingChanged = true
|
somethingChanged = true
|
||||||
summary.filterContexts = newFilterContexts
|
summary.filterContexts = newFilterContexts
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import { createSearchIndexFromStatusOrNotification } from './createSearchIndexFromStatusOrNotification'
|
import { createSearchIndexFromStatusOrNotification } from './createSearchIndexFromStatusOrNotification'
|
||||||
import { uniq } from 'lodash-es'
|
|
||||||
|
|
||||||
export function computeFilterContextsForStatusOrNotification (statusOrNotification, filtersWithRegexes) {
|
export function computeFilterContextsForStatusOrNotification (statusOrNotification, contextsToRegex) {
|
||||||
if (!filtersWithRegexes || !filtersWithRegexes.length) {
|
if (!contextsToRegex || !Object.keys(contextsToRegex).length) {
|
||||||
// avoid computing the search index, just bail out
|
// avoid computing the search index, just bail out
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
// the searchIndex is really just a string of text
|
// the searchIndex is really just a string of text
|
||||||
const searchIndex = createSearchIndexFromStatusOrNotification(statusOrNotification)
|
const searchIndex = createSearchIndexFromStatusOrNotification(statusOrNotification)
|
||||||
const res = filtersWithRegexes && uniq(filtersWithRegexes
|
const res = Object.entries(contextsToRegex)
|
||||||
.filter(({ regex }) => regex.test(searchIndex))
|
.filter(([context, regex]) => regex.test(searchIndex))
|
||||||
.map(_ => _.context)
|
.map(([context]) => context)
|
||||||
.flat())
|
|
||||||
|
|
||||||
// return undefined instead of a new array to reduce memory usage of TimelineSummary
|
// return undefined instead of a new array to reduce memory usage of TimelineSummary
|
||||||
return (res && res.length) ? res : undefined
|
return res.length ? res : undefined
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
// copy-pasta'd from mastodon
|
|
||||||
// https://github.com/tootsuite/mastodon/blob/2ff01f7/app/javascript/mastodon/selectors/index.js#L40-L63
|
|
||||||
const escapeRegExp = string =>
|
|
||||||
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
|
||||||
|
|
||||||
export function createRegexFromFilter (filter) {
|
|
||||||
let expr = escapeRegExp(filter.phrase)
|
|
||||||
|
|
||||||
if (filter.whole_word) {
|
|
||||||
if (/^[\w]/.test(expr)) {
|
|
||||||
expr = `\\b${expr}`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/[\w]$/.test(expr)) {
|
|
||||||
expr = `${expr}\\b`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RegExp(expr, 'i')
|
|
||||||
}
|
|
22
src/routes/_utils/createRegexFromFilters.js
Normal file
22
src/routes/_utils/createRegexFromFilters.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// copy-pasta'd from mastodon
|
||||||
|
// https://github.com/tootsuite/mastodon/blob/2ff01f7/app/javascript/mastodon/selectors/index.js#L40-L63
|
||||||
|
const escapeRegExp = string =>
|
||||||
|
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
||||||
|
|
||||||
|
export const createRegexFromFilters = filters => {
|
||||||
|
return new RegExp(filters.map(filter => {
|
||||||
|
let expr = escapeRegExp(filter.phrase)
|
||||||
|
|
||||||
|
if (filter.whole_word) {
|
||||||
|
if (/^[\w]/.test(expr)) {
|
||||||
|
expr = `\\b${expr}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[\w]$/.test(expr)) {
|
||||||
|
expr = `${expr}\\b`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expr
|
||||||
|
}).join('|'), 'i')
|
||||||
|
}
|
|
@ -13,9 +13,9 @@ class TimelineSummary {
|
||||||
// 1. Avoid computing html-to-text (expensive) for users who don't have any filters (probably most users)
|
// 1. Avoid computing html-to-text (expensive) for users who don't have any filters (probably most users)
|
||||||
// 2. Avoiding keeping the entire html-to-text in memory at all times for all summaries
|
// 2. Avoiding keeping the entire html-to-text in memory at all times for all summaries
|
||||||
// 3. Filters probably change infrequently. When they do, we can just update the summaries
|
// 3. Filters probably change infrequently. When they do, we can just update the summaries
|
||||||
const { unexpiredInstanceFiltersWithRegexes } = store.get()
|
const { unexpiredInstanceFilterRegexes } = store.get()
|
||||||
const filtersWithRegexes = unexpiredInstanceFiltersWithRegexes[instanceName]
|
const contextsToRegex = unexpiredInstanceFilterRegexes[instanceName]
|
||||||
this.filterContexts = computeFilterContextsForStatusOrNotification(item, filtersWithRegexes)
|
this.filterContexts = computeFilterContextsForStatusOrNotification(item, contextsToRegex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue