From 3a6882a6930934b93be050c5bf2103c6fb89bc7c Mon Sep 17 00:00:00 2001 From: Marcin Cichocki Date: Mon, 17 May 2021 00:31:42 +0200 Subject: [PATCH] feat(client-electron): cache fragments during calibration fixes #81 --- configs/webpack.config.main.ts | 2 +- configs/webpack.config.renderer.ts | 2 +- configs/webpack.config.worker.ts | 2 +- src/client-electron/common.ts | 9 +- .../main/{main.ts => index.ts} | 0 .../renderer/{renderer.tsx => index.tsx} | 4 +- .../renderer/pages/Calibrate.tsx | 15 +- .../renderer/pages/CalibrateFragment.tsx | 4 +- src/client-electron/worker/index.ts | 10 ++ src/client-electron/worker/worker.ts | 144 +++++++++++------- 10 files changed, 125 insertions(+), 67 deletions(-) rename src/client-electron/main/{main.ts => index.ts} (100%) rename src/client-electron/renderer/{renderer.tsx => index.tsx} (82%) create mode 100644 src/client-electron/worker/index.ts diff --git a/configs/webpack.config.main.ts b/configs/webpack.config.main.ts index da1b7ab3..771e9526 100644 --- a/configs/webpack.config.main.ts +++ b/configs/webpack.config.main.ts @@ -4,7 +4,7 @@ import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; export const config: webpack.Configuration = { mode: 'development', - entry: join(__dirname, '../src/client-electron/main/main.ts'), + entry: join(__dirname, '../src/client-electron/main/index.ts'), target: 'electron-main', resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], diff --git a/configs/webpack.config.renderer.ts b/configs/webpack.config.renderer.ts index 21d4d3d2..fee0f505 100644 --- a/configs/webpack.config.renderer.ts +++ b/configs/webpack.config.renderer.ts @@ -5,7 +5,7 @@ import webpack from 'webpack'; export const config: webpack.Configuration = { mode: 'development', - entry: join(__dirname, '../src/client-electron/renderer/renderer.tsx'), + entry: join(__dirname, '../src/client-electron/renderer/index.tsx'), target: 'electron-renderer', output: { path: join(__dirname, '../dist'), diff --git a/configs/webpack.config.worker.ts b/configs/webpack.config.worker.ts index c2d19140..4590eecd 100644 --- a/configs/webpack.config.worker.ts +++ b/configs/webpack.config.worker.ts @@ -5,7 +5,7 @@ import webpack from 'webpack'; export const config: webpack.Configuration = { mode: 'development', - entry: join(__dirname, '../src/client-electron/worker/worker.ts'), + entry: join(__dirname, '../src/client-electron/worker/index.ts'), target: 'electron-renderer', output: { path: join(__dirname, '../dist'), diff --git a/src/client-electron/common.ts b/src/client-electron/common.ts index 673e6d60..6356da5c 100644 --- a/src/client-electron/common.ts +++ b/src/client-electron/common.ts @@ -72,7 +72,7 @@ export interface Response { origin: Origin; } -export function createDispatcher(origin: Origin) { +export function createAsyncRequestDispatcher(origin: Origin) { return (action: Omit, 'origin' | 'uuid'>) => new Promise((resolve) => { const uuid = uuidv4(); @@ -91,9 +91,10 @@ export function createDispatcher(origin: Origin) { }); } -export const rendererDispatcher = createDispatcher('renderer'); +export const rendererAsyncRequestDispatcher = + createAsyncRequestDispatcher('renderer'); -export function createListener(origin: Origin) { +export function createAsyncRequestListener(origin: Origin) { return (handler: (req: Request) => Promise) => { ipc.on('async-request', async (event, req: Request) => { const data = await handler(req); @@ -108,7 +109,7 @@ export function createListener(origin: Origin) { }; } -export const workerListener = createListener('worker'); +export const workerAsyncRequestListener = createAsyncRequestListener('worker'); export interface TestThresholdData { fileName: string; diff --git a/src/client-electron/main/main.ts b/src/client-electron/main/index.ts similarity index 100% rename from src/client-electron/main/main.ts rename to src/client-electron/main/index.ts diff --git a/src/client-electron/renderer/renderer.tsx b/src/client-electron/renderer/index.tsx similarity index 82% rename from src/client-electron/renderer/renderer.tsx rename to src/client-electron/renderer/index.tsx index d2769327..2ec8def3 100644 --- a/src/client-electron/renderer/renderer.tsx +++ b/src/client-electron/renderer/index.tsx @@ -1,9 +1,9 @@ -import ReactDOM from 'react-dom'; +import { render } from 'react-dom'; import { App } from './app'; import { GlobalStyles } from './styles/global'; import { HashRouter as Router } from 'react-router-dom'; -ReactDOM.render( +render( <> diff --git a/src/client-electron/renderer/pages/Calibrate.tsx b/src/client-electron/renderer/pages/Calibrate.tsx index 7ef5f6c7..46c445ed 100644 --- a/src/client-electron/renderer/pages/Calibrate.tsx +++ b/src/client-electron/renderer/pages/Calibrate.tsx @@ -1,4 +1,5 @@ -import { FC } from 'react'; +import { rendererAsyncRequestDispatcher as dispatch } from '@/client-electron/common'; +import { FC, useEffect } from 'react'; import { MdKeyboardBackspace } from 'react-icons/md'; import { Link, Route, useRouteMatch } from 'react-router-dom'; import styled from 'styled-components'; @@ -18,11 +19,23 @@ const Heading = styled.h1<{ active: boolean }>` margin: 0; `; +function useContainerInit(fileName: string) { + useEffect(() => { + // FIXME: tiny race condition. Disable button until fragments are ready. + dispatch({ type: 'TEST_THRESHOLD_INIT', data: fileName }); + + return () => { + dispatch({ type: 'TEST_THRESHOLD_DISPOSE' }); + }; + }, []); +} + export const Calibrate: FC = () => { const entry = useHistoryEntryFromParam(); if (!entry) return null; + useContainerInit(entry.fileName); const { path, params } = useRouteMatch<{ entryId: string }>(); const { time, distance } = transformTimestamp(entry.startedAt); diff --git a/src/client-electron/renderer/pages/CalibrateFragment.tsx b/src/client-electron/renderer/pages/CalibrateFragment.tsx index 9d0cc26f..671f2a93 100644 --- a/src/client-electron/renderer/pages/CalibrateFragment.tsx +++ b/src/client-electron/renderer/pages/CalibrateFragment.tsx @@ -3,7 +3,7 @@ import { BreachProtocolFragmentResults, FragmentId } from '@/core'; import { FC, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { rendererDispatcher } from '../../common'; +import { rendererAsyncRequestDispatcher as dispatch } from '../../common'; import { fromCamelCase } from '../common'; import { Col, @@ -55,7 +55,7 @@ export const CalibrateFragment: FC = ({ entry }) => { async function onTestThreshold(value: any) { setLoading(true); - const result = await rendererDispatcher< + const result = await dispatch< BreachProtocolFragmentResults[number], TestThresholdData >({ diff --git a/src/client-electron/worker/index.ts b/src/client-electron/worker/index.ts new file mode 100644 index 00000000..a256d8c4 --- /dev/null +++ b/src/client-electron/worker/index.ts @@ -0,0 +1,10 @@ +import { BreachProtocolWorker } from './worker'; + +const worker = new BreachProtocolWorker(); + +// TODO: error handling in worker. +worker.bootstrap(); + +window.addEventListener('unload', () => { + worker.dispose(); +}); diff --git a/src/client-electron/worker/worker.ts b/src/client-electron/worker/worker.ts index 0dc0fae4..59409b1b 100644 --- a/src/client-electron/worker/worker.ts +++ b/src/client-electron/worker/worker.ts @@ -4,88 +4,122 @@ import { BreachProtocolDaemonsFragment, BreachProtocolGridFragment, BreachProtocolOCRFragment, - FragmentId, SharpImageContainer, } from '@/core'; -import { ipcRenderer as ipc } from 'electron'; -import { listDisplays } from 'screenshot-desktop'; +import { ipcRenderer as ipc, IpcRendererEvent } from 'electron'; +import { listDisplays, ScreenshotDisplayOutput } from 'screenshot-desktop'; import sharp from 'sharp'; import { Action, Request, TestThresholdData, - workerListener, + workerAsyncRequestListener, WorkerStatus, } from '../common'; import { BreachProtocolAutosolver } from './autosolver'; -function updateStatus(payload: WorkerStatus) { - dispatch({ type: 'SET_STATUS', payload }); -} +export class BreachProtocolWorker { + private activeDisplayId: string = null; -function dispatch(action: Omit) { - ipc.send('state', { ...action, origin: 'worker' }); -} + private disposeAsyncRequestListener: () => void = null; + + private fragments: { + grid: BreachProtocolGridFragment; + daemons: BreachProtocolDaemonsFragment; + bufferSize: BreachProtocolBufferSizeFragment; + } = null; + + async bootstrap() { + this.registerListeners(); + + this.updateStatus(WorkerStatus.Bootstrap); + + // TODO: change lang, or remove it completly. + setLang('en'); + const displays = await listDisplays(); + this.activeDisplayId = displays[0].id; -ipc.on('SET_ACTIVE_DISPLAY', (event, state) => { - screenId = state.id; -}); + this.dispatch({ type: 'SET_DISPLAYS', payload: displays }); + this.dispatch({ type: 'SET_ACTIVE_DISPLAY', payload: displays[0] }); -const disposeAsyncRequestListener = workerListener(handleAsyncRequest); + await BreachProtocolOCRFragment.initScheduler(); -async function handleAsyncRequest(req: Request) { - switch (req.type) { - case 'TEST_THRESHOLD': - return testThreshold(req); - default: + this.updateStatus(WorkerStatus.Ready); + ipc.send('worker:ready'); } -} -function getFragment(id: FragmentId, container: SharpImageContainer) { - switch (id) { - case 'grid': - return new BreachProtocolGridFragment(container); - case 'daemons': - return new BreachProtocolDaemonsFragment(container); - case 'bufferSize': - return new BreachProtocolBufferSizeFragment(container); + dispose() { + ipc.removeAllListeners('worker:solve'); + ipc.removeAllListeners('SET_ACTIVE_DISPLAY'); + + this.disposeAsyncRequestListener(); + this.disposeTestThreshold(); } -} -async function testThreshold(req: Request) { - const instance = sharp(req.data.fileName); - const container = await SharpImageContainer.create(instance); - const fragment = getFragment(req.data.fragmentId, container); + private registerListeners() { + ipc.on('worker:solve', this.onWorkerSolve.bind(this)); + ipc.on('SET_ACTIVE_DISPLAY', this.onSetActiveDisplay.bind(this)); - return fragment.recognize(req.data.threshold, false); -} + this.disposeAsyncRequestListener = workerAsyncRequestListener( + this.handleAsyncRequest.bind(this) + ); + } -let screenId: string = null; + private async onWorkerSolve() { + this.updateStatus(WorkerStatus.Working); -async function bootstrap() { - updateStatus(WorkerStatus.Bootstrap); + const bpa = new BreachProtocolAutosolver(this.activeDisplayId); + await bpa.solve(); - setLang('en'); - const displays = await listDisplays(); - screenId = displays[0].id; + this.dispatch({ type: 'ADD_HISTORY_ENTRY', payload: bpa.toJSON() }); + this.updateStatus(WorkerStatus.Ready); + } - dispatch({ type: 'SET_DISPLAYS', payload: displays }); - dispatch({ type: 'SET_ACTIVE_DISPLAY', payload: displays[0] }); + private onSetActiveDisplay( + e: IpcRendererEvent, + display: ScreenshotDisplayOutput + ) { + this.activeDisplayId = display.id; + } - await BreachProtocolOCRFragment.initScheduler(); + private async handleAsyncRequest(req: Request) { + switch (req.type) { + case 'TEST_THRESHOLD_INIT': + return this.initTestThreshold(req); + case 'TEST_THRESHOLD_DISPOSE': + return this.disposeTestThreshold(); + case 'TEST_THRESHOLD': + return this.testThreshold(req); + default: + } + } - ipc.send('worker:ready'); - updateStatus(WorkerStatus.Ready); -} + private async initTestThreshold(req: Request) { + const instance = sharp(req.data); + const container = await SharpImageContainer.create(instance); -bootstrap(); + this.fragments = { + grid: new BreachProtocolGridFragment(container), + daemons: new BreachProtocolDaemonsFragment(container), + bufferSize: new BreachProtocolBufferSizeFragment(container), + }; + } -ipc.on('worker:solve', async () => { - updateStatus(WorkerStatus.Working); + private disposeTestThreshold() { + this.fragments = null; + } - const bpa = new BreachProtocolAutosolver(screenId); - await bpa.solve(); + private async testThreshold(req: Request) { + const fragment = this.fragments[req.data.fragmentId]; - dispatch({ type: 'ADD_HISTORY_ENTRY', payload: bpa.toJSON() }); - updateStatus(WorkerStatus.Ready); -}); + return fragment.recognize(req.data.threshold, false); + } + + private updateStatus(payload: WorkerStatus) { + this.dispatch({ type: 'SET_STATUS', payload }); + } + + private dispatch(action: Omit) { + ipc.send('state', { ...action, origin: 'worker' }); + } +}