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",
|
"performance-now": "^2.1.0",
|
||||||
"pinch-zoom-element": "^1.1.1",
|
"pinch-zoom-element": "^1.1.1",
|
||||||
"preact": "^10.0.0-beta.1",
|
"preact": "^10.0.0-beta.1",
|
||||||
|
"promise-worker": "^2.0.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"quick-lru": "^4.0.1",
|
"quick-lru": "^4.0.1",
|
||||||
"remount": "^0.11.0",
|
"remount": "^0.11.0",
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { database } from '../_database/database'
|
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) {
|
async function getNotification (instanceName, timelineType, timelineValue, itemId) {
|
||||||
return {
|
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) {
|
export function createMakeProps (instanceName, timelineType, timelineValue) {
|
||||||
let taskCount = 0
|
let taskCount = 0
|
||||||
let pending = []
|
let pending = []
|
||||||
|
|
||||||
|
tryInitBlurhash() // start the blurhash worker a bit early to save time
|
||||||
|
|
||||||
// The worker-powered indexeddb promises can resolve in arbitrary order,
|
// The worker-powered indexeddb promises can resolve in arbitrary order,
|
||||||
// causing the timeline to load in a jerky way. With this function, we
|
// 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.
|
// 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) => {
|
return (itemId) => {
|
||||||
taskCount++
|
taskCount++
|
||||||
const promise = timelineType === 'notifications'
|
|
||||||
? getNotification(instanceName, timelineType, timelineValue, itemId)
|
|
||||||
: getStatus(instanceName, timelineType, timelineValue, itemId)
|
|
||||||
|
|
||||||
return promise.then(res => {
|
return fetchFromIndexedDB(itemId)
|
||||||
return awaitAllTasksComplete().then(() => res)
|
.then(decodeAllBlurhashes)
|
||||||
|
.then(statusOrNotification => {
|
||||||
|
return awaitAllTasksComplete().then(() => statusOrNotification)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,16 +110,11 @@
|
||||||
import LazyImage from '../LazyImage.html'
|
import LazyImage from '../LazyImage.html'
|
||||||
import AutoplayVideo from '../AutoplayVideo.html'
|
import AutoplayVideo from '../AutoplayVideo.html'
|
||||||
import { registerClickDelegate } from '../../_utils/delegate'
|
import { registerClickDelegate } from '../../_utils/delegate'
|
||||||
import { decode } from '../../_utils/blurhash'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async oncreate () {
|
async oncreate () {
|
||||||
const { elementId, media } = this.get()
|
const { elementId } = this.get()
|
||||||
registerClickDelegate(this, elementId, () => this.onClick())
|
registerClickDelegate(this, elementId, () => this.onClick())
|
||||||
|
|
||||||
if (media.blurhash) {
|
|
||||||
this.set({ decodedBlurhash: await decode(media.blurhash) })
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
focus: ({ meta }) => meta && meta.focus,
|
focus: ({ meta }) => meta && meta.focus,
|
||||||
|
@ -150,6 +145,7 @@ export default {
|
||||||
elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`,
|
elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`,
|
||||||
description: ({ media }) => media.description || '',
|
description: ({ media }) => media.description || '',
|
||||||
previewUrl: ({ media }) => media.preview_url,
|
previewUrl: ({ media }) => media.preview_url,
|
||||||
|
decodedBlurhash: ({ media }) => media.decodedBlurhash || ONE_TRANSPARENT_PIXEL,
|
||||||
blurhash: ({ showBlurhash, decodedBlurhash }) => showBlurhash && decodedBlurhash,
|
blurhash: ({ showBlurhash, decodedBlurhash }) => showBlurhash && decodedBlurhash,
|
||||||
url: ({ media }) => media.url,
|
url: ({ media }) => media.url,
|
||||||
type: ({ media }) => media.type
|
type: ({ media }) => media.type
|
||||||
|
@ -166,7 +162,6 @@ export default {
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
|
oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
|
||||||
decodedBlurhash: ONE_TRANSPARENT_PIXEL,
|
|
||||||
mouseover: void 0
|
mouseover: void 0
|
||||||
}),
|
}),
|
||||||
store: () => store,
|
store: () => store,
|
||||||
|
|
1
src/routes/_static/blurhash.js
Normal file
1
src/routes/_static/blurhash.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const BLURHASH_RESOLUTION = 32
|
|
@ -1,51 +1,49 @@
|
||||||
import BlurhashWorker from 'worker-loader!../_workers/blurhash' // eslint-disable-line
|
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 worker
|
||||||
let canvas
|
let canvas
|
||||||
let canvasContext2D
|
let canvasContext2D
|
||||||
|
|
||||||
export function init () {
|
export function init () {
|
||||||
worker = worker || new BlurhashWorker()
|
worker = worker || new PromiseWorker(new BlurhashWorker())
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decode (blurhash) {
|
function initCanvas () {
|
||||||
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) {
|
if (!canvas) {
|
||||||
canvas = document.createElement('canvas')
|
canvas = document.createElement('canvas')
|
||||||
canvas.height = RESOLUTION
|
canvas.height = RESOLUTION
|
||||||
canvas.width = RESOLUTION
|
canvas.width = RESOLUTION
|
||||||
canvasContext2D = canvas.getContext('2d')
|
canvasContext2D = canvas.getContext('2d')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// canvas is the backup if we can't use OffscreenCanvas
|
||||||
|
async function decodeUsingCanvas (imageData) {
|
||||||
|
initCanvas()
|
||||||
canvasContext2D.putImageData(imageData, 0, 0)
|
canvasContext2D.putImageData(imageData, 0, 0)
|
||||||
canvas.toBlob(blob => {
|
const blob = await new Promise(resolve => canvas.toBlob(resolve))
|
||||||
resolve(URL.createObjectURL(blob))
|
return URL.createObjectURL(blob)
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
worker.addEventListener('message', onMessage)
|
async function decodeWithoutCache (blurhash) {
|
||||||
worker.postMessage({ encoded: blurhash })
|
init()
|
||||||
} catch (e) {
|
const { decoded, imageData } = await worker.postMessage(blurhash)
|
||||||
reject(e)
|
if (decoded) {
|
||||||
|
return decoded
|
||||||
}
|
}
|
||||||
})
|
return decodeUsingCanvas(imageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decode (blurhash) {
|
||||||
|
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 { 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'
|
const OFFSCREEN_CANVAS = typeof OffscreenCanvas === 'function'
|
||||||
? new OffscreenCanvas(RESOLUTION, RESOLUTION) : null
|
? new OffscreenCanvas(RESOLUTION, RESOLUTION) : null
|
||||||
const OFFSCREEN_CANVAS_CONTEXT_2D = OFFSCREEN_CANVAS
|
const OFFSCREEN_CANVAS_CONTEXT_2D = OFFSCREEN_CANVAS
|
||||||
? OFFSCREEN_CANVAS.getContext('2d') : null
|
? OFFSCREEN_CANVAS.getContext('2d') : null
|
||||||
const CACHE = new QuickLRU({ maxSize: 100 })
|
|
||||||
|
|
||||||
self.addEventListener('message', ({ data: { encoded } }) => {
|
registerPromiseWorker(async (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)
|
const pixels = decodeBlurHash(encoded, RESOLUTION, RESOLUTION)
|
||||||
|
|
||||||
if (pixels) {
|
if (!pixels) {
|
||||||
|
throw new Error('decode did not return any pixels')
|
||||||
|
}
|
||||||
const imageData = new ImageData(pixels, RESOLUTION, RESOLUTION)
|
const imageData = new ImageData(pixels, RESOLUTION, RESOLUTION)
|
||||||
|
|
||||||
if (OFFSCREEN_CANVAS) {
|
if (OFFSCREEN_CANVAS) {
|
||||||
OFFSCREEN_CANVAS_CONTEXT_2D.putImageData(imageData, 0, 0)
|
OFFSCREEN_CANVAS_CONTEXT_2D.putImageData(imageData, 0, 0)
|
||||||
OFFSCREEN_CANVAS.convertToBlob().then(blob => {
|
const blob = await OFFSCREEN_CANVAS.convertToBlob()
|
||||||
const decoded = URL.createObjectURL(blob)
|
const decoded = URL.createObjectURL(blob)
|
||||||
CACHE.set(encoded, decoded)
|
return { decoded, imageData: null }
|
||||||
postMessage({ encoded, decoded, imageData: null, error: null })
|
|
||||||
}).catch(error => {
|
|
||||||
postMessage({ encoded, decoded: null, imageData: null, error })
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
CACHE.set(encoded, imageData)
|
return { imageData, decoded: null }
|
||||||
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 })
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -5991,6 +5991,11 @@ promise-polyfill@^6.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057"
|
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057"
|
||||||
integrity sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=
|
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:
|
promisify-event@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/promisify-event/-/promisify-event-1.0.0.tgz#bd7523ea06b70162f370979016b53a686c60e90f"
|
resolved "https://registry.yarnpkg.com/promisify-event/-/promisify-event-1.0.0.tgz#bd7523ea06b70162f370979016b53a686c60e90f"
|
||||||
|
|
Loading…
Reference in a new issue