Skip to content

Commit

Permalink
perf: use web worker to decode (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
LittleSound authored Oct 10, 2024
1 parent 971014b commit 4c20f75
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 17 deletions.
36 changes: 24 additions & 12 deletions app/components/Scan.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script lang="ts" setup>
import { toUint8Array } from 'js-base64'
import { binaryToBlock, createDecoder, readFileHeaderMetaFromBuffer } from 'luby-transform'
import { binaryToBlock, readFileHeaderMetaFromBuffer } from 'luby-transform'
import QrScanner from 'qr-scanner'
import { createDecodeWorker } from '~/composables/decode-worker'
import { useKiloBytesNumberFormat } from '~/composables/intlNumberFormat'
import { useBytesRate } from '~/composables/timeseries'
import { CameraSignalStatus } from '~/types'
Expand Down Expand Up @@ -156,7 +157,15 @@ async function updateCameraStatus() {
}
}
const decoder = ref(createDecoder())
const decoderWorker = createDecodeWorker()
onUnmounted(() => decoderWorker.dispose())
const decoderStatus = ref<Awaited<ReturnType<typeof decoderWorker.getStatus>>>({
encodedBlocks: new Set(),
decodedData: [],
encodedCount: 0,
decodedCount: 0,
meta: null!,
})
const k = ref(0)
const bytes = ref(0)
Expand All @@ -170,7 +179,7 @@ const dataUrl = ref<string>()
const dots = useTemplateRef<HTMLDivElement[]>('dots')
const status = ref<number[]>([])
const decodedBlocks = computed(() => status.value.filter(i => i === 1).length)
const receivedBytes = computed(() => decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0))
const receivedBytes = computed(() => decoderStatus.value.encodedCount * (decoderStatus.value.meta?.data.length ?? 0))
const filename = ref<string | undefined>()
const contentType = ref<string | undefined>()
Expand All @@ -182,10 +191,10 @@ const receivedBytesFormatted = useKiloBytesNumberFormat(computed(() => (received
function getStatus() {
const array = Array.from({ length: k.value }, () => 0)
for (let i = 0; i < k.value; i++) {
if (decoder.value.decodedData[i] != null)
if (decoderStatus.value.decodedData[i] != null)
array[i] = 1
}
for (const block of decoder.value.encodedBlocks) {
for (const block of decoderStatus.value.encodedBlocks) {
for (const i of block.indices) {
if (array[i] === 0 || array[i]! > block.indices.length) {
array[i] = block.indices.length
Expand Down Expand Up @@ -231,14 +240,15 @@ function toDataURL(data: Uint8Array | string | any, type: string): string {
}
}
let decoderInitPromise: Promise<any> | undefined
async function scanFrame(result: QrScanner.ScanResult) {
cameraSignalStatus.value = CameraSignalStatus.Ready
if (!result.data)
return
bytesReceived.value += result.data.length
totalValidBytesReceived.value = decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0)
totalValidBytesReceived.value = decoderStatus.value.encodedCount * (decoderStatus.value.meta?.data.length ?? 0)
// Do not process the same QR code twice
if (cached.has(result.data))
Expand All @@ -252,7 +262,7 @@ async function scanFrame(result: QrScanner.ScanResult) {
const data = binaryToBlock(binary)
// Data set changed, reset decoder
if (checksum.value !== data.checksum) {
decoder.value = createDecoder()
decoderInitPromise = decoderWorker.createDecoder()
checksum.value = data.checksum
bytes.value = data.bytes
k.value = data.k
Expand All @@ -268,17 +278,19 @@ async function scanFrame(result: QrScanner.ScanResult) {
else if (endTime.value) {
return
}
await decoderInitPromise
cached.add(result.data)
k.value = data.k
data.indices.map(i => pluse(i))
const success = decoder.value.addBlock(data)
const success = await decoderWorker.addBlock(data)
decoderStatus.value = await decoderWorker.getStatus()
status.value = getStatus()
if (success) {
endTime.value = performance.now()
const merged = decoder.value.getDecoded()!
const merged = (await decoderWorker.getDecoded())!
const [mergedData, meta] = readFileHeaderMetaFromBuffer(merged)
dataUrl.value = toDataURL(mergedData, meta.contentType)
Expand Down Expand Up @@ -350,7 +362,7 @@ function now() {
<span text-neutral-500>Decoded</span>
<span text-right md:text-left>{{ decodedBlocks }}</span>
<span text-neutral-500>Received blocks</span>
<span text-right md:text-left>{{ decoder.encodedCount }}</span>
<span text-right md:text-left>{{ decoderStatus.encodedCount }}</span>
<span text-neutral-500>Expected bytes</span>
<span text-right md:text-left>{{ bytesFormatted }}</span>
<span text-neutral-500>Received bytes</span>
Expand Down Expand Up @@ -427,7 +439,7 @@ function now() {

<Collapsable label="Blocks">
<div flex="~ gap-1 wrap" max-w-150 text-xs>
<div v-for="i, idx of decoder.encodedBlocks" :key="idx" border="~ gray/10 rounded" p1>
<div v-for="i, idx of decoderStatus.encodedBlocks" :key="idx" border="~ gray/10 rounded" p1>
<template v-for="x, idy of i.indices" :key="x">
<span v-if="idy !== 0" op25>, </span>
<span :style="{ color: `hsl(${x * 40}, 40%, 60%)` }">{{ x }}</span>
Expand Down
49 changes: 49 additions & 0 deletions app/composables/decode-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { EncodedBlock } from 'luby-transform'
import type { DecoderWorkerFunctions } from './workers/decode'
import { createBirpc } from 'birpc'
import DecodeWorkerConstructor from './workers/decode?worker'

export function createDecodeWorker() {
const worker = new DecodeWorker()

const rpc = Object.assign(createBirpc<DecoderWorkerFunctions>({}, {
post: worker.decodeWorker.postMessage.bind(worker.decodeWorker),
on: fn => worker.decodeWorker.addEventListener('message', event => fn(event.data)),
}), {
worker,
dispose() {
worker.decodeWorker.terminate()
},
})

return rpc
}

class DecodeWorker {
decodeWorker: Worker
constructor() {
this.decodeWorker = new DecodeWorkerConstructor()
}

initDecoder(data?: EncodedBlock[]) {
this.decodeWorker.postMessage({ type: 'createDecoder', data })
}

addBlock(data: EncodedBlock) {
this.decodeWorker.postMessage({ type: 'addBlock', data })
}

onDecoded(callback: (data: Uint8Array | undefined) => void) {
const eventFn = (event: MessageEvent) => {
const { type, data } = event.data
if (type === 'decoded') {
callback(data)
}
}
this.decodeWorker.addEventListener('message', eventFn)

return () => {
this.decodeWorker.removeEventListener('message', eventFn)
}
}
}
49 changes: 49 additions & 0 deletions app/composables/workers/decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { EncodedBlock, LtDecoder } from 'luby-transform'
import { createBirpc } from 'birpc'
import { createDecoder } from 'luby-transform'

let decoder: LtDecoder

const workerFunctions = {
createDecoder(data?: EncodedBlock[]) {
decoder = createDecoder(data)
},
isInitialized() {
return !!decoder
},
addBlock(...args: Parameters<LtDecoder['addBlock']>) {
checkDecoder()
return decoder.addBlock(...args)
},
propagateDecoded(...args: Parameters<LtDecoder['propagateDecoded']>) {
checkDecoder()
decoder.propagateDecoded(...args)
},
getDecoded(...args: Parameters<LtDecoder['getDecoded']>) {
checkDecoder()
return decoder.getDecoded(...args)
},
getStatus() {
checkDecoder()
return {
decodedCount: decoder.decodedCount,
encodedCount: decoder.encodedCount,
meta: decoder.meta,
decodedData: decoder.decodedData,
encodedBlocks: decoder.encodedBlocks,
}
},
}

export type DecoderWorkerFunctions = typeof workerFunctions

createBirpc(workerFunctions, {
post: globalThis.postMessage,
on: fn => globalThis.onmessage = event => fn(event.data),
})

function checkDecoder() {
if (!decoder) {
throw new Error('Decoder not initialized')
}
}
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ dictionaryDefinitions: []
dictionaries: []
words:
- Attributify
- birpc
- composables
- luby
- Nuxt
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"test": "vitest"
},
"dependencies": {
"birpc": "^0.2.19",
"js-base64": "^3.7.7",
"qr-scanner": "^1.4.2",
"uqr": "^0.1.2"
Expand Down
13 changes: 8 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4c20f75

Please sign in to comment.