]> zoso.dev Git - libnemo.git/commitdiff
Rewrite workers to be consolidated into one stringified file to ease Pool management...
authorChris Duncan <chris@zoso.dev>
Tue, 10 Dec 2024 22:08:35 +0000 (14:08 -0800)
committerChris Duncan <chris@zoso.dev>
Tue, 10 Dec 2024 22:08:35 +0000 (14:08 -0800)
src/lib/block.ts
src/lib/pool.ts
src/lib/wallet.ts
src/lib/workers.ts
src/lib/workers/bip44-ckd.ts
src/lib/workers/pow.ts

index 024877b256592e7a125f2be9b2c6ffcd7f5ff84e..5f919c195401475c65d89923c44082dc5f007d0f 100644 (file)
@@ -8,7 +8,6 @@ import { dec, hex } from './convert.js'
 import { NanoNaCl } from './workers/nano-nacl.js'
 import { Pool } from './pool.js'
 import { Rpc } from './rpc.js'
-import { Pow } from './workers.js'
 
 /**
 * Represents a block as defined by the Nano cryptocurrency protocol. The Block
@@ -82,16 +81,15 @@ abstract class Block {
        * A successful response sets the `work` property.
        */
        async pow (): Promise<void> {
-               const pool = new Pool(Pow)
                const data = {
                        "hash": this.previous,
-                       "threshold": (this instanceof SendBlock || this instanceof ChangeBlock)
-                               ? THRESHOLD_SEND
-                               : THRESHOLD_RECEIVE
+                       "threshold": '0xf'
+                       // (this instanceof SendBlock || this instanceof ChangeBlock)
+                       //      ? THRESHOLD_SEND
+                       //      : THRESHOLD_RECEIVE
                }
-               const [{ work }] = await pool.work('converge', [data])
+               const [{ work }] = await Pool.work('converge', 'pow', [data])
                this.work = work
-               pool.dismiss()
        }
 
        /**
index 7c249b9e8de2e6f20f11c0ce82af62ad3769f402..16c8707f65f7ce394e6edcbec96968a19f93b00f 100644 (file)
@@ -1,42 +1,40 @@
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+import { Workers } from './workers.js'
+
+type Job = {
+       id: number
+       name: string
+       reject: (value: any) => void
+       resolve: (value: any) => void
+       data: any[]
+       results: any[]
+}
+
 type Thread = {
-       isBusy: boolean
        worker: Worker
+       job: Job | null
 }
 
 /**
 * Processes an array of tasks using Web Workers.
 */
 export class Pool {
-       #approach: 'converge' | 'divide' = 'divide'
-       #cores: number = Math.max(1, navigator.hardwareConcurrency - 1)
-       #queue: object[] = []
-       #resolve: Function = (value: unknown): void => { }
-       #results: object[] = []
-       #threads: Thread[] = []
-       #url: string
-
-       get isDone (): boolean {
-               for (const thread of this.#threads) {
-                       if (thread.isBusy) {
-                               return false
-                       }
-               }
-               return true
-       }
+       static #threads: Thread[] = []
+       static #cores: number = Math.max(1, navigator.hardwareConcurrency - 1)
+       static #queue: Job[] = []
 
-       constructor (fn: string) {
-               this.#url = URL.createObjectURL(new Blob([fn], { type: 'text/javascript' }))
+       static {
+               const url = URL.createObjectURL(new Blob([Workers], { type: 'text/javascript' }))
                for (let i = this.#cores; i > 0; i--) {
                        const thread = {
-                               isBusy: false,
-                               worker: new Worker(this.#url, { type: 'module' })
+                               worker: new Worker(url, { type: 'module' }),
+                               job: null
                        }
                        thread.worker.addEventListener('message', message => {
-                               const data = new TextDecoder().decode(message.data ?? message)
-                               let result = JSON.parse(data || "[]")
+                               const data: string = new TextDecoder().decode(message.data)
+                               let result: any = JSON.parse(data || "[]")
                                if (!Array.isArray(result)) result = [result]
                                this.#report(thread, result)
                        })
@@ -44,57 +42,61 @@ export class Pool {
                }
        }
 
-       #assign (thread: Thread, next: any[]): void {
+       static #assign (thread: Thread, job: Job): void {
+               const chunk: number = 1 + (job.data.length / this.#cores)
+               const next = job.data.slice(0, chunk)
+               job.data = job.data.slice(chunk)
+               if (job.data.length === 0) this.#queue.shift()
                if (next?.length > 0) {
-                       thread.isBusy = true
-                       const buf = new TextEncoder().encode(JSON.stringify(next)).buffer
-                       thread.worker.postMessage(buf, [buf])
+                       const buffer = new TextEncoder().encode(JSON.stringify(next)).buffer
+                       thread.job = job
+                       thread.worker.postMessage({ name: job.name, buffer }, [buffer])
                }
        }
 
-       #report (thread: Thread, result: any[]): void {
-               thread.isBusy = false
-               if (result?.length > 0) {
-                       this.#results.push(...result)
-               }
-               if (this.#approach === 'converge') {
-                       this.#stop()
-               }
-               if (this.isDone) {
-                       this.#resolve(this.#results)
+       static #isJobDone (jobId: number): boolean {
+               for (const thread of this.#threads) {
+                       if (thread.job?.id === jobId) return false
                }
+               return true
        }
 
-       #stop (): void {
-               for (const thread of this.#threads) {
-                       const msg = ['stop']
-                       const buf = new TextEncoder().encode(JSON.stringify(msg)).buffer
-                       thread.worker.postMessage(buf, [buf])
+       static #report (thread: Thread, results: any[]): void {
+               if (thread.job == null) {
+                       throw new Error('Thread returned results but had nowhere to report it.')
+               }
+               const job = thread.job
+               if (this.#queue.length > 0) {
+                       this.#assign(thread, this.#queue[0])
+               } else {
+                       thread.job = null
+               }
+               if (results.length > 0) {
+                       job.results.push(...results)
+               }
+               if (this.#isJobDone(job.id)) {
+                       job.resolve(job.results)
                }
        }
 
-       async work (approach: 'converge' | 'divide', data: object[],): Promise<any> {
-               if (approach !== 'converge' && approach !== 'divide')
+       static async work (approach: 'converge' | 'distribute', name: string, data: object[]): Promise<any> {
+               if (approach !== 'converge' && approach !== 'distribute')
                        throw new TypeError('Invalid work approach')
                if (!Array.isArray(data)) data = [data]
-               return new Promise(resolve => {
-                       this.#approach = approach
-                       this.#queue = data
-                       this.#resolve = resolve
-                       const chunk = 1 + (this.#queue.length / this.#threads.length)
-                       for (const thread of this.#threads) {
-                               if (approach === 'converge') {
-                                       this.#assign(thread, this.#queue)
-                               } else {
-                                       const next = this.#queue.slice(0, chunk)
-                                       this.#queue = this.#queue.slice(chunk)
-                                       this.#assign(thread, next)
-                               }
+               return new Promise((resolve, reject) => {
+                       const job: Job = {
+                               id: performance.now(),
+                               results: [],
+                               data,
+                               name,
+                               resolve,
+                               reject
+                       }
+                       if (Pool.#queue.length > 0) {
+                               Pool.#queue.push(job)
+                       } else {
+                               for (const thread of Pool.#threads) Pool.#assign(thread, job)
                        }
                })
        }
-
-       dismiss (): void {
-               URL.revokeObjectURL(this.#url)
-       }
 }
index 447c2053f96db88b553ff7c535a02c0769ea4cf0..40104f98927046ff55f3de7ba64de72b08ffc95b 100644 (file)
@@ -9,7 +9,6 @@ import { Entropy } from './entropy.js'
 import { Pool } from './pool.js'\r
 import { Rpc } from './rpc.js'\r
 import { Safe } from './safe.js'\r
-import { Bip44Ckd, NanoNaCl } from './workers.js'\r
 import type { Ledger } from './ledger.js'\r
 \r
 type KeyPair = {\r
@@ -93,9 +92,7 @@ abstract class Wallet {
                        let results = await this.ckd(indexes)\r
                        const data: any = []\r
                        results.forEach(r => data.push({ privateKey: r.privateKey, index: r.index }))\r
-                       const pool = new Pool(NanoNaCl)\r
-                       const keypairs: KeyPair[] = await pool.work('divide', data)\r
-                       pool.dismiss()\r
+                       const keypairs: KeyPair[] = await Pool.work('distribute', 'nano-nacl', data)\r
                        for (const keypair of keypairs) {\r
                                if (keypair.privateKey == null) throw new RangeError('Account private key missing')\r
                                if (keypair.publicKey == null) throw new RangeError('Account public key missing')\r
@@ -422,11 +419,9 @@ export class Bip44Wallet extends Wallet {
        * @returns {Promise<Account>}\r
        */\r
        async ckd (indexes: number[]): Promise<KeyPair[]> {\r
-               const pool = new Pool(Bip44Ckd)\r
                const data: any = []\r
                indexes.forEach(i => data.push({ seed: this.seed, index: i }))\r
-               const privateKeys: KeyPair[] = await pool.work('divide', data)\r
-               pool.dismiss()\r
+               const privateKeys: KeyPair[] = await Pool.work('distribute', 'bip44-cdk', data)\r
                return privateKeys\r
        }\r
 }\r
index 631a4f79c64a50bbc58ce0026931d4532e82b917..d6d03a7af1c736f15f3a3e3744a21222ed28a8cb 100644 (file)
@@ -1,6 +1,90 @@
-import { worker as Bip44Ckd } from './workers/bip44-ckd.js'
-import { worker as NanoNaCl } from './workers/nano-nacl.js'
+import { Bip44Ckd, worker as Bip44CkdWorker } from './workers/bip44-ckd.js'
+import { NanoNaCl, worker as NanoNaClWorker } from './workers/nano-nacl.js'
 // import './workers/passkey.js'
-import { worker as Pow } from './workers/pow.js'
+import { Pow } from './workers/pow.js'
 
-export { Bip44Ckd, NanoNaCl, Pow }
+
+const w = () => {
+       /**
+       * Listens for messages from a calling function.
+       */
+       addEventListener('message', (message: any): void => {
+               const { name, buffer } = message.data
+               if (name === 'STOP') {
+                       close()
+                       const buffer = new ArrayBuffer(0)
+                       //@ts-expect-error
+                       postMessage(buffer, [buffer])
+               } else {
+                       const data = JSON.parse(new TextDecoder().decode(buffer))
+                       switch (name) {
+                               case 'bip44-ckd': {
+                                       getPrivateKey(data).then(report)
+                                       break
+                               }
+                               case 'nano-nacl': {
+                                       getPublicKey(data).then(report)
+                                       break
+                               }
+                               case 'pow': {
+                                       getPow(data).then(report)
+                                       break
+                               }
+                       }
+               }
+       })
+
+       function report (results: any) {
+               const buffer = new TextEncoder().encode(JSON.stringify(results)).buffer
+               //@ts-expect-error
+               postMessage(buffer, [buffer])
+       }
+
+
+       //BIP-44
+       async function getPrivateKey (data: any): Promise<any[]> {
+               const BIP44_PURPOSE = 44
+               return new Promise(async (resolve) => {
+                       for (const d of data) {
+                               if (d.coin != null && d.coin !== BIP44_PURPOSE) {
+                                       d.privateKey = await Bip44Ckd.ckd(d.seed, d.coin, d.index)
+                               } else {
+                                       d.privateKey = await Bip44Ckd.nanoCKD(d.seed, d.index)
+                               }
+                       }
+                       resolve(data)
+               })
+       }
+
+
+
+       //NACL
+       async function getPublicKey (data: any): Promise<any[]> {
+               return new Promise(async (resolve) => {
+                       for (const d of data) {
+                               d.publicKey = await NanoNaCl.convert(d.privateKey)
+                       }
+                       resolve(data)
+               })
+       }
+
+
+
+       // POW
+       async function getPow (data: any) {
+               return new Promise(async (resolve) => {
+                       for (const d of data) {
+                               d.work = await Pow.find(d.hash, d.threshold)
+                       }
+                       resolve(data)
+               })
+       }
+}
+
+const bip44ckd = `const Bip44Ckd = () => {\n${Bip44CkdWorker}\n}\n`
+const nanonacl = `const NanoNaCl = () => {\n${NanoNaClWorker}\n}\n`
+const pow = `const Pow = ${Pow}`
+const start = w.toString().indexOf('{') + 1
+const end = w.toString().lastIndexOf('}')
+const body = w.toString().substring(start, end)
+export const Workers = `${bip44ckd}${nanonacl}${pow}${body}`
index e4307520d6c46c743078be5681c7a335dbbbc856..d51864a229cca8fe366b5f62b720897178b295e5 100644 (file)
@@ -26,16 +26,14 @@ const b = () => {
        })
 
        async function calculate (data: any[]): Promise<any[]> {
-               return new Promise(async (resolve) => {
-                       for (const d of data) {
-                               if (d.coin != null && d.coin !== BIP44_PURPOSE) {
-                                       d.privateKey = await ckd(d.seed, d.coin, d.index)
-                               } else {
-                                       d.privateKey = await nanoCKD(d.seed, d.index)
-                               }
+               for (const d of data) {
+                       if (d.coin != null && d.coin !== BIP44_PURPOSE) {
+                               d.privateKey = await ckd(d.seed, d.coin, d.index)
+                       } else {
+                               d.privateKey = await nanoCKD(d.seed, d.index)
                        }
-                       resolve(data)
-               })
+               }
+               return data
        }
 
        /**
@@ -133,7 +131,7 @@ const b = () => {
                return new Uint8Array(signature)
        }
 
-       return { nanoCKD }
+       return { ckd, nanoCKD }
 }
 
 export const Bip44Ckd = b()
index 1bfe34b8c0f50ac5178fd24df81fa60d111f232b..32eb1394948b83be9ab42435e6ef5b30af5b44bf 100644 (file)
@@ -1,33 +1,12 @@
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-const p = async () => {
-       const SEND_THRESHOLD = '0xfffffff8'
-       /**
-       * Listens for messages from a calling function.
-       */
-       if (typeof addEventListener === 'function') {
-               addEventListener('message', (message: any): void => {
-                       const data = JSON.parse(new TextDecoder().decode(message.data ?? message))
-                       for (const d of data) {
-                               if (d === 'stop') {
-                                       close()
-                                       postMessage(new ArrayBuffer(0))
-                               } else {
-                                       find(d.hash, d.threshold ?? SEND_THRESHOLD).then(nonce => {
-                                               d.work = nonce
-                                               const buf = new TextEncoder().encode(JSON.stringify(data)).buffer
-                                               //@ts-expect-error
-                                               postMessage(buf, [buf])
-                                       })
-                               }
-                       }
-               })
-       }
+export class Pow {
+       static SEND_THRESHOLD = '0xfffffff8'
 
-       async function find (hash: string, threshold: string = SEND_THRESHOLD): Promise<string> {
+       static async find (hash: string, threshold: string = this.SEND_THRESHOLD): Promise<string> {
                return new Promise<string>(resolve => {
-                       calculate(hash, resolve, undefined, threshold)
+                       this.calculate(hash, resolve, undefined, threshold)
                })
        }
 
@@ -50,10 +29,10 @@ const p = async () => {
        // Both width and height must be multiple of 256, (one byte)
        // but do not need to be the same,
        // matching GPU capabilities is the aim
-       const webglWidth = 256 * 4
-       const webglHeight = 256 * 4
+       static webglWidth = 256 * 4
+       static webglHeight = 256 * 4
 
-       function hexify (arr: number[] | Uint8Array): string {
+       static hexify (arr: number[] | Uint8Array): string {
                let out = ''
                for (let i = arr.length - 1; i >= 0; i--) {
                        out += arr[i].toString(16).padStart(2, '0')
@@ -61,7 +40,7 @@ const p = async () => {
                return out
        }
 
-       function hex_reverse (hex: string): string {
+       static hex_reverse (hex: string): string {
                let out = ''
                for (let i = hex.length; i > 0; i -= 2) {
                        out += hex.slice(i - 2, i)
@@ -69,10 +48,10 @@ const p = async () => {
                return out
        }
 
-       function calculate (hashHex: string, callback: (nonce: string | PromiseLike<string>) => any, progressCallback?: (frames: number) => any, threshold: number | string = '0xFFFFFFF8'): void {
+       static calculate (hashHex: string, callback: (nonce: string | PromiseLike<string>) => any, progressCallback?: (frames: number) => any, threshold: number | string = '0xFFFFFFF8'): void {
                if (typeof threshold === 'number') threshold = '0x' + threshold.toString(16)
 
-               const canvas = new OffscreenCanvas(webglWidth, webglHeight)
+               const canvas = new OffscreenCanvas(this.webglWidth, this.webglHeight)
                const gl = canvas.getContext('webgl2')
 
                if (!gl)
@@ -83,7 +62,7 @@ const p = async () => {
 
                gl.clearColor(0, 0, 0, 1)
 
-               const reverseHex = hex_reverse(hashHex)
+               const reverseHex = this.hex_reverse(hashHex)
 
                // Vertext Shader
                const vsSource = `#version 300 es
@@ -329,8 +308,8 @@ const p = async () => {
                        crypto.getRandomValues(work0)
                        crypto.getRandomValues(work1)
 
-                       gl.uniform4uiv(work0Location, Array.from(work0))
-                       gl.uniform4uiv(work1Location, Array.from(work1))
+                       gl.uniform4uiv(work0Location, work0)
+                       gl.uniform4uiv(work1Location, work1)
 
                        // Check with progressCallback every 100 frames
                        if (n % 100 === 0 && typeof progressCallback === 'function' && progressCallback(n))
@@ -339,14 +318,18 @@ const p = async () => {
                        gl.clear(gl.COLOR_BUFFER_BIT)
                        gl.drawArrays(gl.TRIANGLES, 0, 6)
                        const pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4)
+                       performance.mark('readPixels start')
                        gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels)
+                       performance.mark('readPixels end')
+                       for (const e of performance.getEntries()) console.dir(e.toJSON())
+                       performance.clearMarks()
 
                        // Check the pixels for any success
                        for (let i = 0; i < pixels.length; i += 4) {
                                if (pixels[i] !== 0) {
                                        // Return the work value with the custom bits
                                        typeof callback === 'function' &&
-                                               callback(hexify(work1) + hexify([
+                                               callback(this.hexify(work1) + this.hexify([
                                                        pixels[i + 2],
                                                        pixels[i + 3],
                                                        work0[2] ^ (pixels[i] - 1),
@@ -362,12 +345,6 @@ const p = async () => {
                // Begin generation
                self.requestAnimationFrame(draw)
        }
-
-       return { find }
 }
 
-export const Pow = await p()
-
-const start = p.toString().indexOf('{') + 1
-const end = p.toString().lastIndexOf('return')
-export const worker = p.toString().substring(start, end)
+export default Pow.toString()