fix: move blurhash worker operations to before status rendering (#1391)
* fix: move blurhash worker operations to before status rendering * slight refactor * avoid sending encoded data back and forth * move cache outside worker
This commit is contained in:
parent
daa1978945
commit
f8180e813f
|
@ -83,6 +83,7 @@
|
|||
"performance-now": "^2.1.0",
|
||||
"pinch-zoom-element": "^1.1.1",
|
||||
"preact": "^10.0.0-beta.1",
|
||||
"promise-worker": "^2.0.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"quick-lru": "^4.0.1",
|
||||
"remount": "^0.11.0",
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { database } from '../_database/database'
|
||||
import { decode as decodeBlurhash, init as initBlurhash } from '../_utils/blurhash'
|
||||
import { mark, stop } from '../_utils/marks'
|
||||
|
||||
async function getNotification (instanceName, timelineType, timelineValue, itemId) {
|
||||
return {
|
||||
|
@ -16,10 +18,38 @@ async function getStatus (instanceName, timelineType, timelineValue, itemId) {
|
|||
}
|
||||
}
|
||||
|
||||
function tryInitBlurhash () {
|
||||
try {
|
||||
initBlurhash()
|
||||
} catch (err) {
|
||||
console.error('could not start blurhash worker', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function decodeAllBlurhashes (statusOrNotification) {
|
||||
const status = statusOrNotification.status || statusOrNotification.notification.status
|
||||
if (status && status.media_attachments) {
|
||||
mark(`decodeBlurhash-${status.id}`)
|
||||
await Promise.all(status.media_attachments.map(async media => {
|
||||
if (media.blurhash) {
|
||||
try {
|
||||
media.decodedBlurhash = await decodeBlurhash(media.blurhash)
|
||||
} catch (err) {
|
||||
console.warn('Could not decode blurhash, ignoring', err)
|
||||
}
|
||||
}
|
||||
}))
|
||||
stop(`decodeBlurhash-${status.id}`)
|
||||
}
|
||||
return statusOrNotification
|
||||
}
|
||||
|
||||
export function createMakeProps (instanceName, timelineType, timelineValue) {
|
||||
let taskCount = 0
|
||||
let pending = []
|
||||
|
||||
tryInitBlurhash() // start the blurhash worker a bit early to save time
|
||||
|
||||
// The worker-powered indexeddb promises can resolve in arbitrary order,
|
||||
// causing the timeline to load in a jerky way. With this function, we
|
||||
// wait for all promises to resolve before resolving them all in one go.
|
||||
|
@ -34,14 +64,25 @@ export function createMakeProps (instanceName, timelineType, timelineValue) {
|
|||
})
|
||||
}
|
||||
|
||||
async function fetchFromIndexedDB (itemId) {
|
||||
mark(`fetchFromIndexedDB-${itemId}`)
|
||||
try {
|
||||
const res = await (timelineType === 'notifications'
|
||||
? getNotification(instanceName, timelineType, timelineValue, itemId)
|
||||
: getStatus(instanceName, timelineType, timelineValue, itemId))
|
||||
return res
|
||||
} finally {
|
||||
stop(`fetchFromIndexedDB-${itemId}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (itemId) => {
|
||||
taskCount++
|
||||
const promise = timelineType === 'notifications'
|
||||
? getNotification(instanceName, timelineType, timelineValue, itemId)
|
||||
: getStatus(instanceName, timelineType, timelineValue, itemId)
|
||||
|
||||
return promise.then(res => {
|
||||
return awaitAllTasksComplete().then(() => res)
|
||||
})
|
||||
return fetchFromIndexedDB(itemId)
|
||||
.then(decodeAllBlurhashes)
|
||||
.then(statusOrNotification => {
|
||||
return awaitAllTasksComplete().then(() => statusOrNotification)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,16 +110,11 @@
|
|||
import LazyImage from '../LazyImage.html'
|
||||
import AutoplayVideo from '../AutoplayVideo.html'
|
||||
import { registerClickDelegate } from '../../_utils/delegate'
|
||||
import { decode } from '../../_utils/blurhash'
|
||||
|
||||
export default {
|
||||
async oncreate () {
|
||||
const { elementId, media } = this.get()
|
||||
const { elementId } = this.get()
|
||||
registerClickDelegate(this, elementId, () => this.onClick())
|
||||
|
||||
if (media.blurhash) {
|
||||
this.set({ decodedBlurhash: await decode(media.blurhash) })
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
focus: ({ meta }) => meta && meta.focus,
|
||||
|
@ -150,6 +145,7 @@ export default {
|
|||
elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`,
|
||||
description: ({ media }) => media.description || '',
|
||||
previewUrl: ({ media }) => media.preview_url,
|
||||
decodedBlurhash: ({ media }) => media.decodedBlurhash || ONE_TRANSPARENT_PIXEL,
|
||||
blurhash: ({ showBlurhash, decodedBlurhash }) => showBlurhash && decodedBlurhash,
|
||||
url: ({ media }) => media.url,
|
||||
type: ({ media }) => media.type
|
||||
|
@ -166,7 +162,6 @@ export default {
|
|||
},
|
||||
data: () => ({
|
||||
oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
|
||||
decodedBlurhash: ONE_TRANSPARENT_PIXEL,
|
||||
mouseover: void 0
|
||||
}),
|
||||
store: () => store,
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const BLURHASH_RESOLUTION = 32
|
|
@ -1,51 +1,49 @@
|
|||
import BlurhashWorker from 'worker-loader!../_workers/blurhash' // eslint-disable-line
|
||||
import PromiseWorker from 'promise-worker'
|
||||
import { BLURHASH_RESOLUTION as RESOLUTION } from '../_static/blurhash'
|
||||
import QuickLRU from 'quick-lru'
|
||||
|
||||
const CACHE = new QuickLRU({ maxSize: 100 })
|
||||
|
||||
const RESOLUTION = 32
|
||||
let worker
|
||||
let canvas
|
||||
let canvasContext2D
|
||||
|
||||
export function init () {
|
||||
worker = worker || new BlurhashWorker()
|
||||
worker = worker || new PromiseWorker(new BlurhashWorker())
|
||||
}
|
||||
|
||||
function initCanvas () {
|
||||
if (!canvas) {
|
||||
canvas = document.createElement('canvas')
|
||||
canvas.height = RESOLUTION
|
||||
canvas.width = RESOLUTION
|
||||
canvasContext2D = canvas.getContext('2d')
|
||||
}
|
||||
}
|
||||
|
||||
// canvas is the backup if we can't use OffscreenCanvas
|
||||
async function decodeUsingCanvas (imageData) {
|
||||
initCanvas()
|
||||
canvasContext2D.putImageData(imageData, 0, 0)
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve))
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
async function decodeWithoutCache (blurhash) {
|
||||
init()
|
||||
const { decoded, imageData } = await worker.postMessage(blurhash)
|
||||
if (decoded) {
|
||||
return decoded
|
||||
}
|
||||
return decodeUsingCanvas(imageData)
|
||||
}
|
||||
|
||||
export async function decode (blurhash) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
init()
|
||||
|
||||
const onMessage = ({ data: { encoded, decoded, imageData, error } }) => {
|
||||
if (encoded !== blurhash) {
|
||||
return
|
||||
}
|
||||
|
||||
worker.removeEventListener('message', onMessage)
|
||||
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
if (decoded) {
|
||||
resolve(decoded)
|
||||
} else {
|
||||
if (!canvas) {
|
||||
canvas = document.createElement('canvas')
|
||||
canvas.height = RESOLUTION
|
||||
canvas.width = RESOLUTION
|
||||
canvasContext2D = canvas.getContext('2d')
|
||||
}
|
||||
|
||||
canvasContext2D.putImageData(imageData, 0, 0)
|
||||
canvas.toBlob(blob => {
|
||||
resolve(URL.createObjectURL(blob))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
worker.addEventListener('message', onMessage)
|
||||
worker.postMessage({ encoded: blurhash })
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
let result = CACHE.get(blurhash)
|
||||
if (!result) {
|
||||
result = await decodeWithoutCache(blurhash)
|
||||
CACHE.set(blurhash, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1,45 +1,26 @@
|
|||
import { decode as decodeBlurHash } from 'blurhash'
|
||||
import QuickLRU from 'quick-lru'
|
||||
import registerPromiseWorker from 'promise-worker/register'
|
||||
import { BLURHASH_RESOLUTION as RESOLUTION } from '../_static/blurhash'
|
||||
|
||||
const RESOLUTION = 32
|
||||
const OFFSCREEN_CANVAS = typeof OffscreenCanvas === 'function'
|
||||
? new OffscreenCanvas(RESOLUTION, RESOLUTION) : null
|
||||
const OFFSCREEN_CANVAS_CONTEXT_2D = OFFSCREEN_CANVAS
|
||||
? OFFSCREEN_CANVAS.getContext('2d') : null
|
||||
const CACHE = new QuickLRU({ maxSize: 100 })
|
||||
|
||||
self.addEventListener('message', ({ data: { encoded } }) => {
|
||||
try {
|
||||
if (CACHE.has(encoded)) {
|
||||
if (OFFSCREEN_CANVAS) {
|
||||
postMessage({ encoded, decoded: CACHE.get(encoded), imageData: null, error: null })
|
||||
} else {
|
||||
postMessage({ encoded, imageData: CACHE.get(encoded), decoded: null, error: null })
|
||||
}
|
||||
} else {
|
||||
const pixels = decodeBlurHash(encoded, RESOLUTION, RESOLUTION)
|
||||
registerPromiseWorker(async (encoded) => {
|
||||
const pixels = decodeBlurHash(encoded, RESOLUTION, RESOLUTION)
|
||||
|
||||
if (pixels) {
|
||||
const imageData = new ImageData(pixels, RESOLUTION, RESOLUTION)
|
||||
if (!pixels) {
|
||||
throw new Error('decode did not return any pixels')
|
||||
}
|
||||
const imageData = new ImageData(pixels, RESOLUTION, RESOLUTION)
|
||||
|
||||
if (OFFSCREEN_CANVAS) {
|
||||
OFFSCREEN_CANVAS_CONTEXT_2D.putImageData(imageData, 0, 0)
|
||||
OFFSCREEN_CANVAS.convertToBlob().then(blob => {
|
||||
const decoded = URL.createObjectURL(blob)
|
||||
CACHE.set(encoded, decoded)
|
||||
postMessage({ encoded, decoded, imageData: null, error: null })
|
||||
}).catch(error => {
|
||||
postMessage({ encoded, decoded: null, imageData: null, error })
|
||||
})
|
||||
} else {
|
||||
CACHE.set(encoded, imageData)
|
||||
postMessage({ encoded, imageData, decoded: null, error: null })
|
||||
}
|
||||
} else {
|
||||
postMessage({ encoded, decoded: null, imageData: null, error: new Error('decode did not return any pixels') })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
postMessage({ encoded, decoded: null, imageData: null, error })
|
||||
if (OFFSCREEN_CANVAS) {
|
||||
OFFSCREEN_CANVAS_CONTEXT_2D.putImageData(imageData, 0, 0)
|
||||
const blob = await OFFSCREEN_CANVAS.convertToBlob()
|
||||
const decoded = URL.createObjectURL(blob)
|
||||
return { decoded, imageData: null }
|
||||
} else {
|
||||
return { imageData, decoded: null }
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5991,6 +5991,11 @@ promise-polyfill@^6.0.1:
|
|||
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057"
|
||||
integrity sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=
|
||||
|
||||
promise-worker@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/promise-worker/-/promise-worker-2.0.1.tgz#63bb532624ecd40cdb335b51bb7830c3c892aa6c"
|
||||
integrity sha512-jR7vHqMEwWJ15i9vA3qyCKwRHihyLJp1sAa3RyY5F35m3u5s2lQUfq0nzVjbA8Xc7+3mL3Y9+9MHBO9UFRpFxA==
|
||||
|
||||
promisify-event@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/promisify-event/-/promisify-event-1.0.0.tgz#bd7523ea06b70162f370979016b53a686c60e90f"
|
||||
|
|
Loading…
Reference in New Issue