lazily render statuses, use lru cache on top of idb

This commit is contained in:
Nolan Lawson 2018-01-23 18:15:14 -08:00
parent 8555e9e4c1
commit 5f12322ac8
9 changed files with 103 additions and 32 deletions

5
package-lock.json generated
View File

@ -5590,6 +5590,11 @@
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
}, },
"quick-lru": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz",
"integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g="
},
"randomatic": { "randomatic": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz",

View File

@ -38,6 +38,7 @@
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",
"performance-now": "^2.1.0", "performance-now": "^2.1.0",
"pify": "^3.0.0", "pify": "^3.0.0",
"quick-lru": "^1.1.0",
"requestidlecallback": "^0.3.0", "requestidlecallback": "^0.3.0",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"sapper": "^0.3.2", "sapper": "^0.3.2",

View File

@ -1,7 +1,8 @@
<:Window bind:online /> <:Window bind:online />
<div class="timeline" role="feed" aria-label="{{label}}" on:initialized> <div class="timeline" role="feed" aria-label="{{label}}" on:initialized>
<VirtualList component="{{StatusListItem}}" <VirtualList component="{{StatusListItem}}"
items="{{keyedStatuses}}" :makeProps
:items
on:scrollToBottom="onScrollToBottom()" on:scrollToBottom="onScrollToBottom()"
shown="{{initialized}}" shown="{{initialized}}"
footerComponent="{{LoadingFooter}}" footerComponent="{{LoadingFooter}}"
@ -42,16 +43,18 @@
data: () => ({ data: () => ({
StatusListItem: StatusListItem, StatusListItem: StatusListItem,
LoadingFooter: LoadingFooter, LoadingFooter: LoadingFooter,
statuses: [], statusIds: [],
runningUpdate: false, runningUpdate: false,
initialized: false initialized: false
}), }),
computed: { computed: {
keyedStatuses: (statuses) => statuses.map(status => ({ makeProps: ($currentInstance) => (statusId) => database.getStatus($currentInstance, statusId),
props: status, items: (statusIds) => {
key: status.id return statusIds.map(statusId => ({
})), key: statusId
lastStatusId: (statuses) => statuses.length && statuses[statuses.length - 1].id, }))
},
lastStatusId: (statusIds) => statusIds.length && statusIds[statusIds.length - 1],
label: (timeline, $currentInstance) => { label: (timeline, $currentInstance) => {
if (timelines[timeline]) { if (timelines[timeline]) {
`${timelines[timeline].label} timeline for ${$currentInstance}` `${timelines[timeline].label} timeline for ${$currentInstance}`
@ -89,12 +92,16 @@
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
console.log('addStatuses()') console.log('addStatuses()')
} }
let statuses = this.get('statuses') let instanceName = this.store.get('instanceName')
if (!statuses) { let timeline = this.get('timeline')
/* no await */ database.insertStatuses(instanceName, timeline, newStatuses)
let statusIds = this.get('statusIds')
if (!statusIds) {
return return
} }
let merged = mergeStatuses(statuses, newStatuses) let newStatusIds = newStatuses.map(status => status.id)
this.set({ statuses: merged }) let merged = mergeStatuses(statusIds, newStatusIds)
this.set({ statusIds: merged })
}, },
async fetchStatusesAndPossiblyFallBack() { async fetchStatusesAndPossiblyFallBack() {
let online = this.get('online') let online = this.get('online')

View File

@ -1,9 +1,9 @@
<!-- TODO: setting height is hacky, just make this element the scroller --> <!-- TODO: setting height is hacky, just make this element the scroller -->
<div class="virtual-list {{shown ? '' : 'hidden'}}" style="height: {{$height}}px;"> <div class="virtual-list {{shown ? '' : 'hidden'}}" style="height: {{$height}}px;">
{{#each $visibleItems as item @key}} {{#each $visibleItems as item @key}}
<VirtualListItem :component <VirtualListLazyItem :component
offset="{{item.offset}}" offset="{{item.offset}}"
props="{{item.props}}" makeProps="{{makeProps}}"
key="{{item.key}}" key="{{item.key}}"
index="{{item.index}}" index="{{item.index}}"
/> />
@ -19,7 +19,7 @@
} }
</style> </style>
<script> <script>
import VirtualListItem from './VirtualListItem' import VirtualListLazyItem from './VirtualListLazyItem'
import VirtualListFooter from './VirtualListFooter.html' import VirtualListFooter from './VirtualListFooter.html'
import { virtualListStore } from '../_utils/virtualListStore' import { virtualListStore } from '../_utils/virtualListStore'
import throttle from 'lodash/throttle' import throttle from 'lodash/throttle'
@ -62,7 +62,7 @@
}), }),
store: () => virtualListStore, store: () => virtualListStore,
components: { components: {
VirtualListItem, VirtualListLazyItem,
VirtualListFooter VirtualListFooter
}, },
computed: { computed: {

View File

@ -2,7 +2,10 @@
virtual-list-key="{{key}}" virtual-list-key="{{key}}"
ref:node ref:node
style="transform: translateY({{offset}}px);" > style="transform: translateY({{offset}}px);" >
<:Component {component} virtualProps="{{props}}" virtualIndex="{{index}}" virtualLength="{{$numItems}}" <:Component {component}
virtualProps="{{props}}"
virtualIndex="{{index}}"
virtualLength="{{$numItems}}"
on:recalculateHeight="doRecalculateHeight()"/> on:recalculateHeight="doRecalculateHeight()"/>
</div> </div>
<style> <style>

View File

@ -0,0 +1,22 @@
{{#if props}}
<VirtualListItem :component
:offset
:props
:key
:index
/>
{{/if}}
<script>
import VirtualListItem from './VirtualListItem'
export default {
async oncreate() {
let makeProps = this.get('makeProps')
let key = this.get('key')
let props = await makeProps(key)
this.set({ props: props })
},
components: {
VirtualListItem
}
}
</script>

View File

@ -13,6 +13,18 @@ import {
STATUSES_STORE, ACCOUNTS_STORE STATUSES_STORE, ACCOUNTS_STORE
} from './constants' } from './constants'
import QuickLRU from 'quick-lru'
const statusesCache = new QuickLRU({maxSize: 100})
if (process.browser && process.env.NODE_ENV !== 'production') {
window.cacheStats = {
cache: statusesCache,
cacheHits: 0,
cacheMisses: 0
}
}
export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) { export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) {
const db = await getDatabase(instanceName, timeline) const db = await getDatabase(instanceName, timeline)
return await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE], 'readonly', (stores, callback) => { return await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE], 'readonly', (stores, callback) => {
@ -37,6 +49,9 @@ export async function getTimeline(instanceName, timeline, maxId = null, limit =
} }
export async function insertStatuses(instanceName, timeline, statuses) { export async function insertStatuses(instanceName, timeline, statuses) {
for (let status of statuses) {
statusesCache.set(status.id, status)
}
const db = await getDatabase(instanceName, timeline) const db = await getDatabase(instanceName, timeline)
await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE, ACCOUNTS_STORE], 'readwrite', (stores) => { await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE, ACCOUNTS_STORE], 'readwrite', (stores) => {
let [ timelineStore, statusesStore, accountsStore ] = stores let [ timelineStore, statusesStore, accountsStore ] = stores
@ -86,3 +101,23 @@ export async function getAccount(instanceName, accountId) {
export async function clearDatabaseForInstance(instanceName) { export async function clearDatabaseForInstance(instanceName) {
await deleteDatabase(instanceName) await deleteDatabase(instanceName)
} }
export async function getStatus(instanceName, statusId) {
if (statusesCache.has(statusId)) {
if (process.browser && process.env.NODE_ENV !== 'production') {
window.cacheStats.cacheHits++
}
return statusesCache.get(statusId)
}
const db = await getDatabase(instanceName)
let result = await dbPromise(db, STATUSES_STORE, 'readonly', (store, callback) => {
store.get(statusId).onsuccess = (e) => {
callback(e.target.result && e.target.result)
}
})
statusesCache.set(statusId, result)
if (process.browser && process.env.NODE_ENV !== 'production') {
window.cacheStats.cacheMisses++
}
return result
}

View File

@ -1,27 +1,26 @@
// Merge two lists of statuses for the same timeline, e.g. one from IDB // Merge two lists of statuses for the same timeline, e.g. one from IDB
// and another from the network. In case of duplicates, prefer the fresh. // and another from the network. In case of duplicates, prefer the fresh.
export function mergeStatuses(leftStatuses, rightStatuses) { export function mergeStatuses(leftStatusIds, rightStatusIds) {
let leftIndex = 0 let leftIndex = 0
let rightIndex = 0 let rightIndex = 0
let merged = [] let merged = []
while (leftIndex < leftStatuses.length || rightIndex < rightStatuses.length) { while (leftIndex < leftStatusIds.length || rightIndex < rightStatusIds.length) {
if (leftIndex === leftStatuses.length) { if (leftIndex === leftStatusIds.length) {
merged.push(rightStatuses[rightIndex]) merged.push(rightStatusIds[rightIndex])
rightIndex++ rightIndex++
continue continue
} }
if (rightIndex === rightStatuses.length) { if (rightIndex === rightStatusIds.length) {
merged.push(leftStatuses[leftIndex]) merged.push(leftStatusIds[leftIndex])
leftIndex++ leftIndex++
continue continue
} }
let left = leftStatuses[leftIndex] let left = leftStatusIds[leftIndex]
let right = rightStatuses[rightIndex] let right = rightStatusIds[rightIndex]
if (right.id === left.id) { if (right === left) {
merged.push(right.pinafore_stale ? left : right)
rightIndex++ rightIndex++
leftIndex++ leftIndex++
} else if (parseInt(right.id, 10) > parseInt(left.id, 10)) { } else if (parseInt(right, 10) > parseInt(left, 10)) {
merged.push(right) merged.push(right)
rightIndex++ rightIndex++
} else { } else {

View File

@ -56,7 +56,7 @@ virtualListStore.compute('visibleItems',
let len = items.length let len = items.length
let i = -1 let i = -1
while (++i < len) { while (++i < len) {
let { props, key } = items[i] let { key } = items[i]
let height = itemHeights[key] || 0 let height = itemHeights[key] || 0
let currentOffset = totalOffset let currentOffset = totalOffset
totalOffset += height totalOffset += height
@ -72,7 +72,6 @@ virtualListStore.compute('visibleItems',
} }
visibleItems.push({ visibleItems.push({
offset: currentOffset, offset: currentOffset,
props: props,
key: key, key: key,
index: i index: i
}) })