Skip to content

Commit 4c20f75

Browse files
authored
perf: use web worker to decode (#12)
1 parent 971014b commit 4c20f75

File tree

6 files changed

+132
-17
lines changed

6 files changed

+132
-17
lines changed

app/components/Scan.vue

+24-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<script lang="ts" setup>
22
import { toUint8Array } from 'js-base64'
3-
import { binaryToBlock, createDecoder, readFileHeaderMetaFromBuffer } from 'luby-transform'
4-
3+
import { binaryToBlock, readFileHeaderMetaFromBuffer } from 'luby-transform'
54
import QrScanner from 'qr-scanner'
5+
6+
import { createDecodeWorker } from '~/composables/decode-worker'
67
import { useKiloBytesNumberFormat } from '~/composables/intlNumberFormat'
78
import { useBytesRate } from '~/composables/timeseries'
89
import { CameraSignalStatus } from '~/types'
@@ -156,7 +157,15 @@ async function updateCameraStatus() {
156157
}
157158
}
158159
159-
const decoder = ref(createDecoder())
160+
const decoderWorker = createDecodeWorker()
161+
onUnmounted(() => decoderWorker.dispose())
162+
const decoderStatus = ref<Awaited<ReturnType<typeof decoderWorker.getStatus>>>({
163+
encodedBlocks: new Set(),
164+
decodedData: [],
165+
encodedCount: 0,
166+
decodedCount: 0,
167+
meta: null!,
168+
})
160169
161170
const k = ref(0)
162171
const bytes = ref(0)
@@ -170,7 +179,7 @@ const dataUrl = ref<string>()
170179
const dots = useTemplateRef<HTMLDivElement[]>('dots')
171180
const status = ref<number[]>([])
172181
const decodedBlocks = computed(() => status.value.filter(i => i === 1).length)
173-
const receivedBytes = computed(() => decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0))
182+
const receivedBytes = computed(() => decoderStatus.value.encodedCount * (decoderStatus.value.meta?.data.length ?? 0))
174183
175184
const filename = ref<string | undefined>()
176185
const contentType = ref<string | undefined>()
@@ -182,10 +191,10 @@ const receivedBytesFormatted = useKiloBytesNumberFormat(computed(() => (received
182191
function getStatus() {
183192
const array = Array.from({ length: k.value }, () => 0)
184193
for (let i = 0; i < k.value; i++) {
185-
if (decoder.value.decodedData[i] != null)
194+
if (decoderStatus.value.decodedData[i] != null)
186195
array[i] = 1
187196
}
188-
for (const block of decoder.value.encodedBlocks) {
197+
for (const block of decoderStatus.value.encodedBlocks) {
189198
for (const i of block.indices) {
190199
if (array[i] === 0 || array[i]! > block.indices.length) {
191200
array[i] = block.indices.length
@@ -231,14 +240,15 @@ function toDataURL(data: Uint8Array | string | any, type: string): string {
231240
}
232241
}
233242
243+
let decoderInitPromise: Promise<any> | undefined
234244
async function scanFrame(result: QrScanner.ScanResult) {
235245
cameraSignalStatus.value = CameraSignalStatus.Ready
236246
237247
if (!result.data)
238248
return
239249
240250
bytesReceived.value += result.data.length
241-
totalValidBytesReceived.value = decoder.value.encodedCount * (decoder.value.meta?.data.length ?? 0)
251+
totalValidBytesReceived.value = decoderStatus.value.encodedCount * (decoderStatus.value.meta?.data.length ?? 0)
242252
243253
// Do not process the same QR code twice
244254
if (cached.has(result.data))
@@ -252,7 +262,7 @@ async function scanFrame(result: QrScanner.ScanResult) {
252262
const data = binaryToBlock(binary)
253263
// Data set changed, reset decoder
254264
if (checksum.value !== data.checksum) {
255-
decoder.value = createDecoder()
265+
decoderInitPromise = decoderWorker.createDecoder()
256266
checksum.value = data.checksum
257267
bytes.value = data.bytes
258268
k.value = data.k
@@ -268,17 +278,19 @@ async function scanFrame(result: QrScanner.ScanResult) {
268278
else if (endTime.value) {
269279
return
270280
}
281+
await decoderInitPromise
271282
272283
cached.add(result.data)
273284
k.value = data.k
274285
275286
data.indices.map(i => pluse(i))
276-
const success = decoder.value.addBlock(data)
287+
const success = await decoderWorker.addBlock(data)
288+
decoderStatus.value = await decoderWorker.getStatus()
277289
status.value = getStatus()
278290
if (success) {
279291
endTime.value = performance.now()
280292
281-
const merged = decoder.value.getDecoded()!
293+
const merged = (await decoderWorker.getDecoded())!
282294
const [mergedData, meta] = readFileHeaderMetaFromBuffer(merged)
283295
dataUrl.value = toDataURL(mergedData, meta.contentType)
284296
@@ -350,7 +362,7 @@ function now() {
350362
<span text-neutral-500>Decoded</span>
351363
<span text-right md:text-left>{{ decodedBlocks }}</span>
352364
<span text-neutral-500>Received blocks</span>
353-
<span text-right md:text-left>{{ decoder.encodedCount }}</span>
365+
<span text-right md:text-left>{{ decoderStatus.encodedCount }}</span>
354366
<span text-neutral-500>Expected bytes</span>
355367
<span text-right md:text-left>{{ bytesFormatted }}</span>
356368
<span text-neutral-500>Received bytes</span>
@@ -427,7 +439,7 @@ function now() {
427439

428440
<Collapsable label="Blocks">
429441
<div flex="~ gap-1 wrap" max-w-150 text-xs>
430-
<div v-for="i, idx of decoder.encodedBlocks" :key="idx" border="~ gray/10 rounded" p1>
442+
<div v-for="i, idx of decoderStatus.encodedBlocks" :key="idx" border="~ gray/10 rounded" p1>
431443
<template v-for="x, idy of i.indices" :key="x">
432444
<span v-if="idy !== 0" op25>, </span>
433445
<span :style="{ color: `hsl(${x * 40}, 40%, 60%)` }">{{ x }}</span>

app/composables/decode-worker.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { EncodedBlock } from 'luby-transform'
2+
import type { DecoderWorkerFunctions } from './workers/decode'
3+
import { createBirpc } from 'birpc'
4+
import DecodeWorkerConstructor from './workers/decode?worker'
5+
6+
export function createDecodeWorker() {
7+
const worker = new DecodeWorker()
8+
9+
const rpc = Object.assign(createBirpc<DecoderWorkerFunctions>({}, {
10+
post: worker.decodeWorker.postMessage.bind(worker.decodeWorker),
11+
on: fn => worker.decodeWorker.addEventListener('message', event => fn(event.data)),
12+
}), {
13+
worker,
14+
dispose() {
15+
worker.decodeWorker.terminate()
16+
},
17+
})
18+
19+
return rpc
20+
}
21+
22+
class DecodeWorker {
23+
decodeWorker: Worker
24+
constructor() {
25+
this.decodeWorker = new DecodeWorkerConstructor()
26+
}
27+
28+
initDecoder(data?: EncodedBlock[]) {
29+
this.decodeWorker.postMessage({ type: 'createDecoder', data })
30+
}
31+
32+
addBlock(data: EncodedBlock) {
33+
this.decodeWorker.postMessage({ type: 'addBlock', data })
34+
}
35+
36+
onDecoded(callback: (data: Uint8Array | undefined) => void) {
37+
const eventFn = (event: MessageEvent) => {
38+
const { type, data } = event.data
39+
if (type === 'decoded') {
40+
callback(data)
41+
}
42+
}
43+
this.decodeWorker.addEventListener('message', eventFn)
44+
45+
return () => {
46+
this.decodeWorker.removeEventListener('message', eventFn)
47+
}
48+
}
49+
}

app/composables/workers/decode.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { EncodedBlock, LtDecoder } from 'luby-transform'
2+
import { createBirpc } from 'birpc'
3+
import { createDecoder } from 'luby-transform'
4+
5+
let decoder: LtDecoder
6+
7+
const workerFunctions = {
8+
createDecoder(data?: EncodedBlock[]) {
9+
decoder = createDecoder(data)
10+
},
11+
isInitialized() {
12+
return !!decoder
13+
},
14+
addBlock(...args: Parameters<LtDecoder['addBlock']>) {
15+
checkDecoder()
16+
return decoder.addBlock(...args)
17+
},
18+
propagateDecoded(...args: Parameters<LtDecoder['propagateDecoded']>) {
19+
checkDecoder()
20+
decoder.propagateDecoded(...args)
21+
},
22+
getDecoded(...args: Parameters<LtDecoder['getDecoded']>) {
23+
checkDecoder()
24+
return decoder.getDecoded(...args)
25+
},
26+
getStatus() {
27+
checkDecoder()
28+
return {
29+
decodedCount: decoder.decodedCount,
30+
encodedCount: decoder.encodedCount,
31+
meta: decoder.meta,
32+
decodedData: decoder.decodedData,
33+
encodedBlocks: decoder.encodedBlocks,
34+
}
35+
},
36+
}
37+
38+
export type DecoderWorkerFunctions = typeof workerFunctions
39+
40+
createBirpc(workerFunctions, {
41+
post: globalThis.postMessage,
42+
on: fn => globalThis.onmessage = event => fn(event.data),
43+
})
44+
45+
function checkDecoder() {
46+
if (!decoder) {
47+
throw new Error('Decoder not initialized')
48+
}
49+
}

cspell.config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ dictionaryDefinitions: []
44
dictionaries: []
55
words:
66
- Attributify
7+
- birpc
78
- composables
89
- luby
910
- Nuxt

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"test": "vitest"
1919
},
2020
"dependencies": {
21+
"birpc": "^0.2.19",
2122
"js-base64": "^3.7.7",
2223
"qr-scanner": "^1.4.2",
2324
"uqr": "^0.1.2"

pnpm-lock.yaml

+8-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)