From afd94eee553660f1c7b2d50d3c00aceba9026f9a Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Sep 2018 16:38:21 +0200 Subject: [PATCH 01/10] integrate comlink and convert some performance-intensive functions to use webworkers --- .eslintrc.json | 1 + app/assets/javascripts/libs/request.js | 31 ++++++++++++------- .../bucket_data_handling/wkstore_adapter.js | 28 +++++------------ package.json | 5 ++- webpack.config.js | 4 +++ yarn.lock | 24 +++++++++++++- 6 files changed, 59 insertions(+), 34 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index f11570aeb25..39da8584401 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -57,6 +57,7 @@ "no-restricted-properties": ["off", { "object": "Math", "property": "pow" }], "no-restricted-syntax": "warn", "no-restricted-syntax": ["error", "ForInStatement"], + "no-restricted-globals": "warn", "no-underscore-dangle": "off", "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "no-use-before-define": ["error", { "functions": false, "classes": false }], diff --git a/app/assets/javascripts/libs/request.js b/app/assets/javascripts/libs/request.js index ea051bd12b0..24fbfb238ed 100644 --- a/app/assets/javascripts/libs/request.js +++ b/app/assets/javascripts/libs/request.js @@ -7,6 +7,11 @@ import _ from "lodash"; import Toast from "libs/toast"; import * as Utils from "libs/utils"; import { pingDataStoreIfAppropriate, pingMentionedDataStores } from "admin/datastore_health_check"; +import { createWorker } from "oxalis/workers/comlink_wrapper"; +import handleStatus from "libs/handle_http_status"; +import FetchBufferWorker from "oxalis/workers/fetch_buffer.worker"; + +const fetchBufferViaWorker = createWorker(FetchBufferWorker); type methodType = "GET" | "POST" | "DELETE" | "HEAD" | "OPTIONS" | "PUT" | "PATCH"; @@ -15,6 +20,7 @@ type RequestOptions = { method?: methodType, timeout?: number, compress?: boolean, + useWebworkerForArrayBuffer?: boolean, }; export type RequestOptionsWithData = RequestOptions & { @@ -147,8 +153,11 @@ class Request { receiveArraybuffer = (url: string, options: RequestOptions = {}): Promise => this.triggerRequest( url, - _.defaultsDeep(options, { headers: { Accept: "application/octet-stream" } }), - response => response.arrayBuffer(), + _.defaultsDeep(options, { + headers: { Accept: "application/octet-stream" }, + useWebworkerForArrayBuffer: true, + }), + // response => response.arrayBuffer(), ); // IN: JSON @@ -201,9 +210,14 @@ class Request { } options.headers = headers; - let fetchPromise = fetch(url, options).then(this.handleStatus); - if (responseDataHandler != null) { - fetchPromise = fetchPromise.then(responseDataHandler); + let fetchPromise; + if (options.useWebworkerForArrayBuffer) { + fetchPromise = fetchBufferViaWorker(url, options); + } else { + fetchPromise = fetch(url, options).then(handleStatus); + if (responseDataHandler != null) { + fetchPromise = fetchPromise.then(responseDataHandler); + } } if (!options.doNotCatch) { @@ -228,13 +242,6 @@ class Request { setTimeout(() => resolve("timeout"), timeout); }); - handleStatus = (response: Response): Promise => { - if (response.status >= 200 && response.status < 400) { - return Promise.resolve(response); - } - return Promise.reject(response); - }; - handleError = (requestedUrl: string, error: Response | Error): Promise => { // Check whether this request failed due to a problematic // datastore diff --git a/app/assets/javascripts/oxalis/model/bucket_data_handling/wkstore_adapter.js b/app/assets/javascripts/oxalis/model/bucket_data_handling/wkstore_adapter.js index 374c818a7b0..b020619f59c 100644 --- a/app/assets/javascripts/oxalis/model/bucket_data_handling/wkstore_adapter.js +++ b/app/assets/javascripts/oxalis/model/bucket_data_handling/wkstore_adapter.js @@ -10,9 +10,13 @@ import { doWithToken } from "admin/admin_rest_api"; import type { DataBucket } from "oxalis/model/bucket_data_handling/bucket"; import type { Vector3, Vector4 } from "oxalis/constants"; import type { DataLayerType } from "oxalis/store"; -import { getResolutions, isSegmentationLayer } from "oxalis/model/accessors/dataset_accessor.js"; +import { getResolutions, isSegmentationLayer } from "oxalis/model/accessors/dataset_accessor"; import { bucketPositionToGlobalAddress } from "oxalis/model/helpers/position_converter"; import constants from "oxalis/constants"; +import { createWorker } from "oxalis/workers/comlink_wrapper"; +import DecodeFourBitWorker from "oxalis/workers/decode_four_bit.worker"; + +const decodeFourBit = createWorker(DecodeFourBitWorker); export const REQUEST_TIMEOUT = 30000; @@ -71,30 +75,14 @@ export async function requestFromStore( }, ); - let result = new Uint8Array(responseBuffer); + let resultBuffer = responseBuffer; if (fourBit) { - result = decodeFourBit(result); + resultBuffer = await decodeFourBit(resultBuffer); } - return result; + return new Uint8Array(resultBuffer); }); } -function decodeFourBit(bufferArray: Uint8Array): Uint8Array { - // Expand 4-bit data - const newColors = new Uint8Array(bufferArray.length << 1); - - let index = 0; - while (index < newColors.length) { - const value = bufferArray[index >> 1]; - newColors[index] = value & 0b11110000; - index++; - newColors[index] = value << 4; - index++; - } - - return newColors; -} - export async function sendToStore(batch: Array): Promise { const YIELD_AFTER_X_BUCKETS = 3; let counter = 0; diff --git a/package.json b/package.json index 54dfb95823a..51bc111d7ac 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "base64-js": "^1.2.1", "classnames": "^2.2.5", "clipboard-js": "^0.2.0", + "comlinkjs": "^3.0.3", "data.maybe": "^1.2.2", "deep-for-each": "^2.0.3", "deep-freeze": "0.0.1", @@ -148,11 +149,13 @@ "react-virtualized": "^9.20.1", "redux": "^3.6.0", "redux-saga": "^0.16.0", + "reselect": "^3.0.1", "scroll-into-view-if-needed": "2.2.8", "stats.js": "^1.0.0", "three": "^0.87.0", "tween.js": "^16.3.1", - "whatwg-fetch": "^1.1.0" + "whatwg-fetch": "^1.1.0", + "worker-loader": "^2.0.0" }, "resolutions": { "**/mini-store": "^1.1.0" diff --git a/webpack.config.js b/webpack.config.js index 6c37d5d75b3..80d15295e6a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -51,6 +51,10 @@ module.exports = function(env = {}) { }, module: { rules: [ + { + test: /\.worker\.js$/, + use: { loader: "worker-loader" }, + }, { test: /\.js$/, exclude: /(node_modules|bower_components)/, diff --git a/yarn.lock b/yarn.lock index 562e50b4419..11e359e96e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2523,6 +2523,10 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +comlinkjs@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/comlinkjs/-/comlinkjs-3.0.3.tgz#a976952d9368e5e8c1d6c0ba78f3a9a70df797ed" + comma-separated-tokens@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.4.tgz#72083e58d4a462f01866f6617f4d98a3cd3b8a46" @@ -6135,7 +6139,7 @@ loader-utils@^0.2.16: json5 "^0.5.0" object-assign "^4.0.1" -loader-utils@^1.0.2, loader-utils@^1.1.0: +loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" dependencies: @@ -9265,6 +9269,10 @@ require-uncached@^1.0.3: caller-path "^0.1.0" resolve-from "^1.0.0" +reselect@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" + resize-observer-polyfill@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69" @@ -9456,6 +9464,13 @@ sax@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/sax/-/sax-0.6.1.tgz#563b19c7c1de892e09bfc4f2fc30e3c27f0952b9" +schema-utils@^0.4.0: + version "0.4.7" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" + dependencies: + ajv "^6.1.0" + ajv-keywords "^3.1.0" + schema-utils@^0.4.3, schema-utils@^0.4.4, schema-utils@^0.4.5: version "0.4.5" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e" @@ -11021,6 +11036,13 @@ worker-farm@^1.5.2: dependencies: errno "~0.1.7" +worker-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac" + dependencies: + loader-utils "^1.0.0" + schema-utils "^0.4.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" From bda68db52687c24d5b8ab0b5854d01d69b87489a Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Sep 2018 16:39:04 +0200 Subject: [PATCH 02/10] use reselect to avoid performance-heavy recomputation of max zoomstep (especially noticable when brushing) --- .../model/accessors/dataset_accessor.js | 19 +++++++++++++++++++ .../oxalis/model/accessors/flycam_accessor.js | 18 ++---------------- .../oxalis/model/reducers/flycam_reducer.js | 4 ++-- .../view/settings/user_settings_view.js | 4 ++-- .../test/model/flycam_accessors.spec.js | 3 ++- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js b/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js index f463d55dbc2..12f36f77922 100644 --- a/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js +++ b/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js @@ -6,6 +6,8 @@ import constants, { Vector3Indicies, ModeValues } from "oxalis/constants"; import type { APIDatasetType } from "admin/api_flow_types"; import type { Vector3 } from "oxalis/constants"; import type { SettingsType, DataLayerType } from "oxalis/store"; +import Maybe from "data.maybe"; +import { createSelector } from "reselect"; export function getResolutions(dataset: APIDatasetType): Vector3[] { // Different layers can have different resolutions. At the moment, @@ -32,6 +34,23 @@ export function getResolutions(dataset: APIDatasetType): Vector3[] { return mostExtensiveResolutions.concat(extendedResolutions); } +function _getMaxZoomStep(maybeDataset: ?APIDatasetType): number { + const minimumZoomStepCount = 1; + const maxZoomstep = Maybe.fromNullable(maybeDataset) + .map(dataset => + Math.max( + minimumZoomStepCount, + Math.max(0, ...getResolutions(dataset).map(r => Math.max(r[0], r[1], r[2]))), + ), + ) + .getOrElse(2 ** (minimumZoomStepCount + constants.DOWNSAMPLED_ZOOM_STEP_COUNT - 1)); + return maxZoomstep; +} + +// With createSelector, we ensure that the last return value of _getMaxZoomStep is reused, +// if the dataset didn't change. +export const getMaxZoomStep = createSelector(_getMaxZoomStep, _.identity); + function getDataLayers(dataset: APIDatasetType): DataLayerType[] { return dataset.dataSource.dataLayers; } diff --git a/app/assets/javascripts/oxalis/model/accessors/flycam_accessor.js b/app/assets/javascripts/oxalis/model/accessors/flycam_accessor.js index 448117ddb3f..46a518db74b 100644 --- a/app/assets/javascripts/oxalis/model/accessors/flycam_accessor.js +++ b/app/assets/javascripts/oxalis/model/accessors/flycam_accessor.js @@ -2,14 +2,13 @@ import type { Vector3, OrthoViewType, OrthoViewMapType } from "oxalis/constants"; import type { FlycamType, OxalisState } from "oxalis/store"; import constants, { OrthoViews } from "oxalis/constants"; -import Maybe from "data.maybe"; import Dimensions from "oxalis/model/dimensions"; import * as scaleInfo from "oxalis/model/scaleinfo"; import * as Utils from "libs/utils"; import type { Matrix4x4 } from "libs/mjs"; import { M4x4 } from "libs/mjs"; import * as THREE from "three"; -import { getResolutions } from "oxalis/model/accessors/dataset_accessor"; +import { getMaxZoomStep } from "oxalis/model/accessors/dataset_accessor"; // All methods in this file should use constants.PLANE_WIDTH instead of constants.VIEWPORT_WIDTH // as the area that is rendered is only of size PLANE_WIDTH. @@ -55,21 +54,8 @@ export function getZoomedMatrix(flycam: FlycamType): Matrix4x4 { return M4x4.scale1(flycam.zoomStep, flycam.currentMatrix); } -export function getMaxZoomStep(state: OxalisState): number { - const minimumZoomStepCount = 1; - const maxZoomstep = Maybe.fromNullable(state.dataset) - .map(dataset => - Math.max( - minimumZoomStepCount, - Math.max(0, ...getResolutions(dataset).map(r => Math.max(r[0], r[1], r[2]))), - ), - ) - .getOrElse(2 ** (minimumZoomStepCount + constants.DOWNSAMPLED_ZOOM_STEP_COUNT - 1)); - return maxZoomstep; -} - export function getRequestLogZoomStep(state: OxalisState): number { - const maxLogZoomStep = Math.log2(getMaxZoomStep(state)); + const maxLogZoomStep = Math.log2(getMaxZoomStep(state.dataset)); const min = Math.min(state.datasetConfiguration.quality, maxLogZoomStep); const value = Math.ceil(Math.log2(state.flycam.zoomStep / MAX_ZOOM_STEP_DIFF)) + diff --git a/app/assets/javascripts/oxalis/model/reducers/flycam_reducer.js b/app/assets/javascripts/oxalis/model/reducers/flycam_reducer.js index d7fd4021712..9a49e2f0fe4 100644 --- a/app/assets/javascripts/oxalis/model/reducers/flycam_reducer.js +++ b/app/assets/javascripts/oxalis/model/reducers/flycam_reducer.js @@ -2,7 +2,7 @@ import update from "immutability-helper"; import type { OxalisState } from "oxalis/store"; import type { ActionType } from "oxalis/model/actions/actions"; -import { getMaxZoomStep } from "oxalis/model/accessors/flycam_accessor"; +import { getMaxZoomStep } from "oxalis/model/accessors/dataset_accessor"; import { getBaseVoxelFactors } from "oxalis/model/scaleinfo"; import { M4x4 } from "libs/mjs"; import type { Matrix4x4 } from "libs/mjs"; @@ -114,7 +114,7 @@ function moveReducer(state: OxalisState, vector: Vector3): OxalisState { export function zoomReducer(state: OxalisState, zoomStep: number): OxalisState { return update(state, { flycam: { - zoomStep: { $set: Utils.clamp(ZOOM_STEP_MIN, zoomStep, getMaxZoomStep(state)) }, + zoomStep: { $set: Utils.clamp(ZOOM_STEP_MIN, zoomStep, getMaxZoomStep(state.dataset)) }, }, }); } diff --git a/app/assets/javascripts/oxalis/view/settings/user_settings_view.js b/app/assets/javascripts/oxalis/view/settings/user_settings_view.js index 64f787b19b1..ffe20c059db 100644 --- a/app/assets/javascripts/oxalis/view/settings/user_settings_view.js +++ b/app/assets/javascripts/oxalis/view/settings/user_settings_view.js @@ -28,7 +28,7 @@ import { LogSliderSetting, } from "oxalis/view/settings/setting_input_views"; import { setUserBoundingBoxAction } from "oxalis/model/actions/annotation_actions"; -import { getMaxZoomStep } from "oxalis/model/accessors/flycam_accessor"; +import { getMaxZoomStep } from "oxalis/model/accessors/dataset_accessor"; import { enforceSkeletonTracing, getActiveNode, @@ -369,7 +369,7 @@ const mapStateToProps = (state: OxalisState) => ({ userConfiguration: state.userConfiguration, tracing: state.tracing, zoomStep: state.flycam.zoomStep, - maxZoomStep: getMaxZoomStep(state), + maxZoomStep: getMaxZoomStep(state.dataset), viewMode: state.temporaryConfiguration.viewMode, controlMode: state.temporaryConfiguration.controlMode, brushSize: state.temporaryConfiguration.brushSize, diff --git a/app/assets/javascripts/test/model/flycam_accessors.spec.js b/app/assets/javascripts/test/model/flycam_accessors.spec.js index 76bc59ca092..d1313102cae 100644 --- a/app/assets/javascripts/test/model/flycam_accessors.spec.js +++ b/app/assets/javascripts/test/model/flycam_accessors.spec.js @@ -2,6 +2,7 @@ import test from "ava"; import _ from "lodash"; import * as accessors from "oxalis/model/accessors/flycam_accessor"; +import { getMaxZoomStep } from "oxalis/model/accessors/dataset_accessor"; const initialState = { dataset: { @@ -32,7 +33,7 @@ const initialState = { }; test("Flycam Accessors should calculate the max zoom step", t => { - t.is(accessors.getMaxZoomStep(initialState), 16); + t.is(getMaxZoomStep(initialState.dataset), 16); }); test("Flycam Accessors should calculate the request log zoom step (1/3)", t => { From bda31e0332e7b0b266ca7d537819d182e23984ba Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Wed, 5 Sep 2018 16:47:13 +0200 Subject: [PATCH 03/10] move compress function to webworker --- app/assets/javascripts/libs/request.js | 4 +++- app/assets/javascripts/libs/utils.js | 17 ----------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/libs/request.js b/app/assets/javascripts/libs/request.js index 24fbfb238ed..086b94677c8 100644 --- a/app/assets/javascripts/libs/request.js +++ b/app/assets/javascripts/libs/request.js @@ -10,8 +10,10 @@ import { pingDataStoreIfAppropriate, pingMentionedDataStores } from "admin/datas import { createWorker } from "oxalis/workers/comlink_wrapper"; import handleStatus from "libs/handle_http_status"; import FetchBufferWorker from "oxalis/workers/fetch_buffer.worker"; +import CompressWorker from "oxalis/workers/compress.worker"; const fetchBufferViaWorker = createWorker(FetchBufferWorker); +const compress = createWorker(CompressWorker); type methodType = "GET" | "POST" | "DELETE" | "HEAD" | "OPTIONS" | "PUT" | "PATCH"; @@ -53,7 +55,7 @@ class Request { let body = _.isString(options.data) ? options.data : JSON.stringify(options.data); if (options.compress) { - body = await Utils.compress(body); + body = await compress(body); if (options.headers == null) { options.headers = { "Content-Encoding": "gzip", diff --git a/app/assets/javascripts/libs/utils.js b/app/assets/javascripts/libs/utils.js index 28bc26f66c3..91180ee4683 100644 --- a/app/assets/javascripts/libs/utils.js +++ b/app/assets/javascripts/libs/utils.js @@ -3,7 +3,6 @@ import _ from "lodash"; import type { Vector3, Vector4, Vector6, BoundingBoxType } from "oxalis/constants"; import Maybe from "data.maybe"; import window, { document, location } from "libs/window"; -import pako from "pako"; import naturalSort from "javascript-natural-sort"; import type { APIUserType } from "admin/api_flow_types"; @@ -406,22 +405,6 @@ export function addEventListenerWithDelegation( return { [eventName]: wrapperFunc }; } -export async function compress(data: Uint8Array | string): Promise { - const DEFLATE_PUSH_SIZE = 65536; - - const deflator = new pako.Deflate({ gzip: true }); - for (let offset = 0; offset < data.length; offset += DEFLATE_PUSH_SIZE) { - // The second parameter to push indicates whether this is the last chunk to be deflated - deflator.push( - data.slice(offset, offset + DEFLATE_PUSH_SIZE), - offset + DEFLATE_PUSH_SIZE >= data.length, - ); - // eslint-disable-next-line no-await-in-loop - await sleep(1); - } - return deflator.result; -} - export function median8(dataArray: Array): number { // Returns the median of an already *sorted* array of size 8 (e.g., with sortArray8) return Math.round((dataArray[3] + dataArray[4]) / 2); From c690963f985c7f99c89f1999f03c076ab094dbfc Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Sep 2018 09:08:17 +0200 Subject: [PATCH 04/10] add missing worker files; move convertToBase64 to web worker; simplify compress code --- .../javascripts/libs/handle_http_status.js | 10 ++++++ .../bucket_data_handling/wkstore_adapter.js | 14 +++------ .../workers/byte_array_to_base64.worker.js | 10 ++++++ .../oxalis/workers/comlink_wrapper.js | 23 ++++++++++++++ .../oxalis/workers/compress.worker.js | 11 +++++++ .../oxalis/workers/decode_four_bit.worker.js | 31 +++++++++++++++++++ .../oxalis/workers/fetch_buffer.worker.js | 12 +++++++ .../workers/headers_transfer_handler.js | 19 ++++++++++++ .../javascripts/oxalis/workers/readme.md | 31 +++++++++++++++++++ 9 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/libs/handle_http_status.js create mode 100644 app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js create mode 100644 app/assets/javascripts/oxalis/workers/comlink_wrapper.js create mode 100644 app/assets/javascripts/oxalis/workers/compress.worker.js create mode 100644 app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js create mode 100644 app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js create mode 100644 app/assets/javascripts/oxalis/workers/headers_transfer_handler.js create mode 100644 app/assets/javascripts/oxalis/workers/readme.md diff --git a/app/assets/javascripts/libs/handle_http_status.js b/app/assets/javascripts/libs/handle_http_status.js new file mode 100644 index 00000000000..d734eacaaa7 --- /dev/null +++ b/app/assets/javascripts/libs/handle_http_status.js @@ -0,0 +1,10 @@ +// @flow + +const handleStatus = (response: Response): Promise => { + if (response.status >= 200 && response.status < 400) { + return Promise.resolve(response); + } + return Promise.reject(response); +}; + +export default handleStatus; diff --git a/app/assets/javascripts/oxalis/model/bucket_data_handling/wkstore_adapter.js b/app/assets/javascripts/oxalis/model/bucket_data_handling/wkstore_adapter.js index b020619f59c..ef22c51c38c 100644 --- a/app/assets/javascripts/oxalis/model/bucket_data_handling/wkstore_adapter.js +++ b/app/assets/javascripts/oxalis/model/bucket_data_handling/wkstore_adapter.js @@ -1,11 +1,9 @@ // @flow -import Base64 from "base64-js"; import Request from "libs/request"; import Store from "oxalis/store"; import { pushSaveQueueAction } from "oxalis/model/actions/save_actions"; import { updateBucket } from "oxalis/model/sagas/update_actions"; -import * as Utils from "libs/utils"; import { doWithToken } from "admin/admin_rest_api"; import type { DataBucket } from "oxalis/model/bucket_data_handling/bucket"; import type { Vector3, Vector4 } from "oxalis/constants"; @@ -15,8 +13,10 @@ import { bucketPositionToGlobalAddress } from "oxalis/model/helpers/position_con import constants from "oxalis/constants"; import { createWorker } from "oxalis/workers/comlink_wrapper"; import DecodeFourBitWorker from "oxalis/workers/decode_four_bit.worker"; +import ByteArrayToBase64Worker from "oxalis/workers/byte_array_to_base64.worker"; const decodeFourBit = createWorker(DecodeFourBitWorker); +const byteArrayToBase64 = createWorker(ByteArrayToBase64Worker); export const REQUEST_TIMEOUT = 30000; @@ -84,20 +84,16 @@ export async function requestFromStore( } export async function sendToStore(batch: Array): Promise { - const YIELD_AFTER_X_BUCKETS = 3; - let counter = 0; const items = []; for (const bucket of batch) { - counter++; - // Do not block the main thread for too long as Base64.fromByteArray is performance heavy - // eslint-disable-next-line no-await-in-loop - if (counter % YIELD_AFTER_X_BUCKETS === 0) await Utils.sleep(1); const bucketData = bucket.getData(); const bucketInfo = createSendBucketInfo( bucket.zoomedAddress, getResolutions(Store.getState().dataset), ); - items.push(updateBucket(bucketInfo, Base64.fromByteArray(bucketData))); + // eslint-disable-next-line no-await-in-loop + const base64 = await byteArrayToBase64(bucketData); + items.push(updateBucket(bucketInfo, base64)); } Store.dispatch(pushSaveQueueAction(items, "volume")); } diff --git a/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js b/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js new file mode 100644 index 00000000000..bd3a9581f5c --- /dev/null +++ b/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js @@ -0,0 +1,10 @@ +// @flow +import Base64 from "base64-js"; + +import { expose } from "./comlink_wrapper"; + +export default function byteArrayToBase64(byteArray: Uint8Array): string { + return Base64.fromByteArray(byteArray); +} + +expose(byteArrayToBase64, self); diff --git a/app/assets/javascripts/oxalis/workers/comlink_wrapper.js b/app/assets/javascripts/oxalis/workers/comlink_wrapper.js new file mode 100644 index 00000000000..00d1d5a8230 --- /dev/null +++ b/app/assets/javascripts/oxalis/workers/comlink_wrapper.js @@ -0,0 +1,23 @@ +import { + proxy as _proxy, + transferHandlers as _transferHandlers, + expose as _expose, +} from "comlinkjs"; + +import headersTransferHandler from "oxalis/workers/headers_transfer_handler"; + +_transferHandlers.set("Headers", headersTransferHandler); +export const createWorker: (fn: Fn) => Fn = WorkerClass => + _proxy( + // When importing a worker module, flow doesn't know that a special Worker class + // is imported. Instead, flow thinks that the declared function is + // directly imported. We exploit this by simply typing this createWorker function as an identity function + // (T => T). That way, we gain proper flow typing for functions executed in web workers. However, + // we need to suppress the following flow error for that to work. + // $FlowFixMe + new WorkerClass(), + ); + +export const proxy = _proxy; +export const transferHandlers = _transferHandlers; +export const expose = _expose; diff --git a/app/assets/javascripts/oxalis/workers/compress.worker.js b/app/assets/javascripts/oxalis/workers/compress.worker.js new file mode 100644 index 00000000000..1e993e1ce29 --- /dev/null +++ b/app/assets/javascripts/oxalis/workers/compress.worker.js @@ -0,0 +1,11 @@ +// @flow + +import pako from "pako"; +import { expose } from "./comlink_wrapper"; + +function compress(data: Uint8Array | string): Promise { + return pako.gzip(data); +} + +export default compress; +expose(compress, self); diff --git a/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js b/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js new file mode 100644 index 00000000000..5212ddbfb9c --- /dev/null +++ b/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js @@ -0,0 +1,31 @@ +// @flow + +// This module provides performance intensive functions, which is why it has to be +// imported and used via a webworker like this: +// import { proxy } from "comlinkjs"; +// import DecodeFourBitWorker from ".../workers/decode_four_bit.worker"; +// const decodeFourBit = proxy(new DecodeFourBitWorker); +// await decodeFourBit(...) + +import { expose } from "./comlink_wrapper"; + +// This function receives and returns ArrayBuffer, since that can be transferred without +// copying to/out of the webworker +export default function decodeFourBit(buffer: ArrayBuffer): ArrayBuffer { + const bufferArray = new Uint8Array(buffer); + // Expand 4-bit data + const newColors = new Uint8Array(2 * bufferArray.length); + + let index = 0; + while (index < newColors.length) { + const value = bufferArray[index >> 1]; + newColors[index] = value & 0b11110000; + index++; + newColors[index] = value << 4; + index++; + } + + return newColors.buffer; +} + +expose(decodeFourBit, self); diff --git a/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js b/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js new file mode 100644 index 00000000000..efeb3cddf99 --- /dev/null +++ b/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js @@ -0,0 +1,12 @@ +// @flow + +import handleStatus from "libs/handle_http_status"; +import { expose } from "./comlink_wrapper"; + +export default function fetchBufferViaWebworker(url: RequestInfo, options?: RequestOptions) { + return fetch(url, options) + .then(handleStatus) + .then(response => response.arrayBuffer()); +} + +expose(fetchBufferViaWebworker, self); diff --git a/app/assets/javascripts/oxalis/workers/headers_transfer_handler.js b/app/assets/javascripts/oxalis/workers/headers_transfer_handler.js new file mode 100644 index 00000000000..14c09cdda59 --- /dev/null +++ b/app/assets/javascripts/oxalis/workers/headers_transfer_handler.js @@ -0,0 +1,19 @@ +// @flow + +const headersTransferHandler = { + canHandle(obj: any) { + return obj instanceof Headers; + }, + serialize(obj: Headers): Array<[string, string]> { + return Array.from(obj.entries()); + }, + deserialize(keyValueList: Array<[string, string]>): Headers { + const headers = new Headers(); + for (const [key, value] of keyValueList) { + headers.set(key, value); + } + return headers; + }, +}; + +export default headersTransferHandler; diff --git a/app/assets/javascripts/oxalis/workers/readme.md b/app/assets/javascripts/oxalis/workers/readme.md new file mode 100644 index 00000000000..2bc2975a4e9 --- /dev/null +++ b/app/assets/javascripts/oxalis/workers/readme.md @@ -0,0 +1,31 @@ +# Web Worker Modules + +This folder contains modules which can be executed in the context of web workers. +Using web workers, we can offload heavy computation off the main thread. + +## Using Web Worker Modules + +When using a web worker module, you need to import it and turn it into a + +``` +import SomeFunctionWorker from "oxalis/workers/some_function.worker"; + +const someFunction = createWorker(SomeFunctionWorker); + +// Execute it +const result = await someFunction(parameters); +``` + +## Writing Web Worker Modules + +First, create a new file in this folder and ensure that it has the extension `*.worker.js`. +The module should export a default function (or class) which is `expose`d via [comlink](https://github.com/GoogleChromeLabs/comlink). + +## Caveats + +- Accessing global state (e.g., the Store) is not directly possible from web workers, since they have their own execution context. Pass necessary information into web workers via parameters. +- By default, parameters and return values are either structurally cloned or transfered (if they support it) to/from the web worker. Copying is potentially performance-intensive and also won't propagate any mutations across the main-thread/webworker border. If objects are transferable (e.g., for ArrayBuffers, but not TypedArrays), they are moved to the new context, which means that they cannot be accessed in the old thread, anymore. +- Not all objects can be passed between main thread and web workers (e.g., Header objects). For these cases, you have to implement and register a specific transfer handler for the object type. See `headers_transfer_handler.js` as an example. +- Web worker files can import NPM modules and also modules from within this code base, but beware that the execution context between the main thread and web workers is strictly isolated. Webpack will create a separete JS file for each web worker into which all imported code is compiled. + +Learn more about the Comlink module we use [here](https://github.com/GoogleChromeLabs/comlink). From 08034d98686d11498978bbbe218f7c0a9b38f5e2 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Sep 2018 09:15:09 +0200 Subject: [PATCH 05/10] improve worker file definition format --- app/assets/javascripts/libs/request.js | 1 - .../oxalis/workers/byte_array_to_base64.worker.js | 4 ++-- app/assets/javascripts/oxalis/workers/comlink_wrapper.js | 5 ++++- app/assets/javascripts/oxalis/workers/compress.worker.js | 3 +-- .../javascripts/oxalis/workers/decode_four_bit.worker.js | 4 ++-- app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js | 4 ++-- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/libs/request.js b/app/assets/javascripts/libs/request.js index 086b94677c8..a5a5109591d 100644 --- a/app/assets/javascripts/libs/request.js +++ b/app/assets/javascripts/libs/request.js @@ -5,7 +5,6 @@ import _ from "lodash"; import Toast from "libs/toast"; -import * as Utils from "libs/utils"; import { pingDataStoreIfAppropriate, pingMentionedDataStores } from "admin/datastore_health_check"; import { createWorker } from "oxalis/workers/comlink_wrapper"; import handleStatus from "libs/handle_http_status"; diff --git a/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js b/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js index bd3a9581f5c..b4aefc2b633 100644 --- a/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js +++ b/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js @@ -3,8 +3,8 @@ import Base64 from "base64-js"; import { expose } from "./comlink_wrapper"; -export default function byteArrayToBase64(byteArray: Uint8Array): string { +function byteArrayToBase64(byteArray: Uint8Array): string { return Base64.fromByteArray(byteArray); } -expose(byteArrayToBase64, self); +export default expose(byteArrayToBase64, self); diff --git a/app/assets/javascripts/oxalis/workers/comlink_wrapper.js b/app/assets/javascripts/oxalis/workers/comlink_wrapper.js index 00d1d5a8230..fd841bd8e5b 100644 --- a/app/assets/javascripts/oxalis/workers/comlink_wrapper.js +++ b/app/assets/javascripts/oxalis/workers/comlink_wrapper.js @@ -20,4 +20,7 @@ export const createWorker: (fn: Fn) => Fn = WorkerClass => export const proxy = _proxy; export const transferHandlers = _transferHandlers; -export const expose = _expose; +export const expose = (fn: T, _self: any): T => { + _expose(fn, _self); + return fn; +}; diff --git a/app/assets/javascripts/oxalis/workers/compress.worker.js b/app/assets/javascripts/oxalis/workers/compress.worker.js index 1e993e1ce29..0dad4d283dc 100644 --- a/app/assets/javascripts/oxalis/workers/compress.worker.js +++ b/app/assets/javascripts/oxalis/workers/compress.worker.js @@ -7,5 +7,4 @@ function compress(data: Uint8Array | string): Promise { return pako.gzip(data); } -export default compress; -expose(compress, self); +export default expose(compress, self); diff --git a/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js b/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js index 5212ddbfb9c..35aca33000b 100644 --- a/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js +++ b/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js @@ -11,7 +11,7 @@ import { expose } from "./comlink_wrapper"; // This function receives and returns ArrayBuffer, since that can be transferred without // copying to/out of the webworker -export default function decodeFourBit(buffer: ArrayBuffer): ArrayBuffer { +function decodeFourBit(buffer: ArrayBuffer): ArrayBuffer { const bufferArray = new Uint8Array(buffer); // Expand 4-bit data const newColors = new Uint8Array(2 * bufferArray.length); @@ -28,4 +28,4 @@ export default function decodeFourBit(buffer: ArrayBuffer): ArrayBuffer { return newColors.buffer; } -expose(decodeFourBit, self); +export default expose(decodeFourBit, self); diff --git a/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js b/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js index efeb3cddf99..e40c4f861d3 100644 --- a/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js +++ b/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js @@ -3,10 +3,10 @@ import handleStatus from "libs/handle_http_status"; import { expose } from "./comlink_wrapper"; -export default function fetchBufferViaWebworker(url: RequestInfo, options?: RequestOptions) { +function fetchBufferViaWebworker(url: RequestInfo, options?: RequestOptions) { return fetch(url, options) .then(handleStatus) .then(response => response.arrayBuffer()); } -expose(fetchBufferViaWebworker, self); +export default expose(fetchBufferViaWebworker, self); From b4dafdfbba649de623237c1c666c28dd33f9c601 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Sep 2018 09:47:21 +0200 Subject: [PATCH 06/10] switch from reselect to memoize-one --- .../oxalis/model/accessors/dataset_accessor.js | 6 ++---- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js b/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js index 12f36f77922..ec2a91b78d1 100644 --- a/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js +++ b/app/assets/javascripts/oxalis/model/accessors/dataset_accessor.js @@ -7,7 +7,7 @@ import type { APIDatasetType } from "admin/api_flow_types"; import type { Vector3 } from "oxalis/constants"; import type { SettingsType, DataLayerType } from "oxalis/store"; import Maybe from "data.maybe"; -import { createSelector } from "reselect"; +import memoizeOne from "memoize-one"; export function getResolutions(dataset: APIDatasetType): Vector3[] { // Different layers can have different resolutions. At the moment, @@ -47,9 +47,7 @@ function _getMaxZoomStep(maybeDataset: ?APIDatasetType): number { return maxZoomstep; } -// With createSelector, we ensure that the last return value of _getMaxZoomStep is reused, -// if the dataset didn't change. -export const getMaxZoomStep = createSelector(_getMaxZoomStep, _.identity); +export const getMaxZoomStep = memoizeOne(_getMaxZoomStep); function getDataLayers(dataset: APIDatasetType): DataLayerType[] { return dataset.dataSource.dataLayers; diff --git a/package.json b/package.json index 51bc111d7ac..069ee131d26 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "js-priority-queue": "^0.1.5", "jsonschema": "^1.2.4", "lodash": "^4.13.1", + "memoize-one": "^4.0.2", "mini-css-extract-plugin": "^0.4.0", "mjs": "^1.0.0", "moment": "^2.21.0", @@ -149,7 +150,6 @@ "react-virtualized": "^9.20.1", "redux": "^3.6.0", "redux-saga": "^0.16.0", - "reselect": "^3.0.1", "scroll-into-view-if-needed": "2.2.8", "stats.js": "^1.0.0", "three": "^0.87.0", diff --git a/yarn.lock b/yarn.lock index 11e359e96e2..44cc9aa72d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6462,6 +6462,10 @@ mem@^1.1.0: dependencies: mimic-fn "^1.0.0" +memoize-one@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.2.tgz#3fb8db695aa14ab9c0f1644e1585a8806adc1aee" + memory-fs@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290" @@ -9269,10 +9273,6 @@ require-uncached@^1.0.3: caller-path "^0.1.0" resolve-from "^1.0.0" -reselect@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147" - resize-observer-polyfill@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.0.tgz#660ff1d9712a2382baa2cad450a4716209f9ca69" From e5340396056ea489c62fee855bc77cc1a460bfac Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Sep 2018 09:47:33 +0200 Subject: [PATCH 07/10] improve webworker readme --- .../oxalis/workers/decode_four_bit.worker.js | 7 ------- app/assets/javascripts/oxalis/workers/readme.md | 12 +++++++----- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js b/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js index 35aca33000b..eed460d1ca5 100644 --- a/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js +++ b/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js @@ -1,12 +1,5 @@ // @flow -// This module provides performance intensive functions, which is why it has to be -// imported and used via a webworker like this: -// import { proxy } from "comlinkjs"; -// import DecodeFourBitWorker from ".../workers/decode_four_bit.worker"; -// const decodeFourBit = proxy(new DecodeFourBitWorker); -// await decodeFourBit(...) - import { expose } from "./comlink_wrapper"; // This function receives and returns ArrayBuffer, since that can be transferred without diff --git a/app/assets/javascripts/oxalis/workers/readme.md b/app/assets/javascripts/oxalis/workers/readme.md index 2bc2975a4e9..e5268f7f6ff 100644 --- a/app/assets/javascripts/oxalis/workers/readme.md +++ b/app/assets/javascripts/oxalis/workers/readme.md @@ -1,18 +1,19 @@ # Web Worker Modules This folder contains modules which can be executed in the context of web workers. -Using web workers, we can offload heavy computation off the main thread. +Using web workers, we can offload heavy computation from the main thread. ## Using Web Worker Modules -When using a web worker module, you need to import it and turn it into a +When using a web worker module, you need to import it and turn it into an executable function: -``` +```js +import { createWorker } from "oxalis/workers/comlink_wrapper"; import SomeFunctionWorker from "oxalis/workers/some_function.worker"; const someFunction = createWorker(SomeFunctionWorker); -// Execute it +// Use it const result = await someFunction(parameters); ``` @@ -20,11 +21,12 @@ const result = await someFunction(parameters); First, create a new file in this folder and ensure that it has the extension `*.worker.js`. The module should export a default function (or class) which is `expose`d via [comlink](https://github.com/GoogleChromeLabs/comlink). +See `compress.worker.js` for an example. ## Caveats - Accessing global state (e.g., the Store) is not directly possible from web workers, since they have their own execution context. Pass necessary information into web workers via parameters. -- By default, parameters and return values are either structurally cloned or transfered (if they support it) to/from the web worker. Copying is potentially performance-intensive and also won't propagate any mutations across the main-thread/webworker border. If objects are transferable (e.g., for ArrayBuffers, but not TypedArrays), they are moved to the new context, which means that they cannot be accessed in the old thread, anymore. +- By default, parameters and return values are either structurally cloned or transfered (if they support it) to/from the web worker. Copying is potentially performance-intensive and also won't propagate any mutations across the main-thread/webworker border. If objects are transferable (e.g., for ArrayBuffers, but not TypedArrays), they are moved to the new context, which means that they cannot be accessed in the old thread, anymore. In both cases, care has to be taken. In general, web workers should only be responsible for a very small (but cpu intensive) task with a bare minimum of dependencies. - Not all objects can be passed between main thread and web workers (e.g., Header objects). For these cases, you have to implement and register a specific transfer handler for the object type. See `headers_transfer_handler.js` as an example. - Web worker files can import NPM modules and also modules from within this code base, but beware that the execution context between the main thread and web workers is strictly isolated. Webpack will create a separete JS file for each web worker into which all imported code is compiled. From 3bfb5d58f0376a7a0a1cf63f26337fb19d622faf Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Sep 2018 12:07:42 +0200 Subject: [PATCH 08/10] fix tests for webworkers --- .../workers/byte_array_to_base64.worker.js | 2 +- .../oxalis/workers/comlink_wrapper.js | 50 ++++++++++++++----- .../oxalis/workers/compress.worker.js | 2 +- .../oxalis/workers/decode_four_bit.worker.js | 2 +- .../oxalis/workers/fetch_buffer.worker.js | 2 +- 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js b/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js index b4aefc2b633..afe471443fa 100644 --- a/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js +++ b/app/assets/javascripts/oxalis/workers/byte_array_to_base64.worker.js @@ -7,4 +7,4 @@ function byteArrayToBase64(byteArray: Uint8Array): string { return Base64.fromByteArray(byteArray); } -export default expose(byteArrayToBase64, self); +export default expose(byteArrayToBase64); diff --git a/app/assets/javascripts/oxalis/workers/comlink_wrapper.js b/app/assets/javascripts/oxalis/workers/comlink_wrapper.js index fd841bd8e5b..8809a05a513 100644 --- a/app/assets/javascripts/oxalis/workers/comlink_wrapper.js +++ b/app/assets/javascripts/oxalis/workers/comlink_wrapper.js @@ -1,14 +1,36 @@ -import { - proxy as _proxy, - transferHandlers as _transferHandlers, - expose as _expose, -} from "comlinkjs"; - import headersTransferHandler from "oxalis/workers/headers_transfer_handler"; -_transferHandlers.set("Headers", headersTransferHandler); -export const createWorker: (fn: Fn) => Fn = WorkerClass => - _proxy( +const isNodeContext = typeof process !== "undefined" && process.title !== "browser"; + +function importComlink() { + if (!isNodeContext) { + // eslint-disable-next-line global-require + const { proxy, transferHandlers, expose: _expose } = require("comlinkjs"); + return { proxy, transferHandlers, _expose }; + } else { + return { + proxy: null, + transferHandlers: new Map(), + _expose: null, + }; + } +} + +const { proxy, transferHandlers, _expose } = importComlink(); + +// It's important that transferHandlers are registered in this wrapper module and +// not from another file. Otherwise, callers would need to register the handler +// in the main thread as well as in the web worker. +// Since this wrapper is imported from both sides, the handlers are also registered on both sides. +transferHandlers.set("Headers", headersTransferHandler); + +export const createWorker: (fn: Fn) => Fn = WorkerClass => { + if (isNodeContext) { + // In a node context (e.g., when executing tests), we don't create web workers + return WorkerClass; + } + + return proxy( // When importing a worker module, flow doesn't know that a special Worker class // is imported. Instead, flow thinks that the declared function is // directly imported. We exploit this by simply typing this createWorker function as an identity function @@ -17,10 +39,12 @@ export const createWorker: (fn: Fn) => Fn = WorkerClass => // $FlowFixMe new WorkerClass(), ); +}; -export const proxy = _proxy; -export const transferHandlers = _transferHandlers; -export const expose = (fn: T, _self: any): T => { - _expose(fn, _self); +export const expose = (fn: T): T => { + // In a node context (e.g., when executing tests), we don't create web workers + if (!isNodeContext) { + _expose(fn, self); + } return fn; }; diff --git a/app/assets/javascripts/oxalis/workers/compress.worker.js b/app/assets/javascripts/oxalis/workers/compress.worker.js index 0dad4d283dc..627bf43e5ca 100644 --- a/app/assets/javascripts/oxalis/workers/compress.worker.js +++ b/app/assets/javascripts/oxalis/workers/compress.worker.js @@ -7,4 +7,4 @@ function compress(data: Uint8Array | string): Promise { return pako.gzip(data); } -export default expose(compress, self); +export default expose(compress); diff --git a/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js b/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js index eed460d1ca5..7e0eda704c7 100644 --- a/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js +++ b/app/assets/javascripts/oxalis/workers/decode_four_bit.worker.js @@ -21,4 +21,4 @@ function decodeFourBit(buffer: ArrayBuffer): ArrayBuffer { return newColors.buffer; } -export default expose(decodeFourBit, self); +export default expose(decodeFourBit); diff --git a/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js b/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js index e40c4f861d3..dcbae54d674 100644 --- a/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js +++ b/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js @@ -9,4 +9,4 @@ function fetchBufferViaWebworker(url: RequestInfo, options?: RequestOptions) { .then(response => response.arrayBuffer()); } -export default expose(fetchBufferViaWebworker, self); +export default expose(fetchBufferViaWebworker); From 1fa869ce31301e21c557ffd7d2c9e6aedae06478 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Thu, 6 Sep 2018 13:07:34 +0200 Subject: [PATCH 09/10] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d37cab411f..46e35b74e95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md). - The fallback segmentation layer attribute of volume tracings is now persisted to NML/ZIP files. Upon re-upload, only volume tracings with this attribute will show a fallback layer. Use `tools/volumeAddFallbackLayer.py` to add this attribute to existing volume tracings. - The welcome header will now also show on the default page if there are no existing organisations. [#3133](https://github.com/scalableminds/webknossos/pull/3133) +- Improved general performance of the tracing view by leveraging web workers. [#3162](https://github.com/scalableminds/webknossos/pull/3162) ### Fixed From 59cb9119b078336188adc1d90f0f430249f784e2 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Fri, 7 Sep 2018 11:32:39 +0200 Subject: [PATCH 10/10] ensure that web worker instantiation is always done by doing some flow type magic --- .flowconfig | 1 + .../oxalis/workers/comlink_wrapper.js | 34 +++++++++++++------ .../oxalis/workers/fetch_buffer.worker.js | 2 +- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/.flowconfig b/.flowconfig index 08bea56471e..5f7af4ea376 100644 --- a/.flowconfig +++ b/.flowconfig @@ -24,6 +24,7 @@ [options] suppress_comment=\\(.\\|\n\\)*\\$ExpectError suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe +suppress_comment=\\(.\\|\n\\)*\\$FlowIgnore include_warnings=true module.ignore_non_literal_requires=true diff --git a/app/assets/javascripts/oxalis/workers/comlink_wrapper.js b/app/assets/javascripts/oxalis/workers/comlink_wrapper.js index 8809a05a513..4777111a3a7 100644 --- a/app/assets/javascripts/oxalis/workers/comlink_wrapper.js +++ b/app/assets/javascripts/oxalis/workers/comlink_wrapper.js @@ -1,9 +1,11 @@ +// @flow import headersTransferHandler from "oxalis/workers/headers_transfer_handler"; -const isNodeContext = typeof process !== "undefined" && process.title !== "browser"; - function importComlink() { + const isNodeContext = typeof process !== "undefined" && process.title !== "browser"; if (!isNodeContext) { + // Comlink should only be imported in a browser context, since it makes use of functionality + // which does not exist in node // eslint-disable-next-line global-require const { proxy, transferHandlers, expose: _expose } = require("comlinkjs"); return { proxy, transferHandlers, _expose }; @@ -24,9 +26,20 @@ const { proxy, transferHandlers, _expose } = importComlink(); // Since this wrapper is imported from both sides, the handlers are also registered on both sides. transferHandlers.set("Headers", headersTransferHandler); -export const createWorker: (fn: Fn) => Fn = WorkerClass => { - if (isNodeContext) { - // In a node context (e.g., when executing tests), we don't create web workers +// Worker modules export bare functions, but webpack turns these into Worker classes which need to be +// instantiated first. +// To ensure that code always executes the necessary instantiation, we cheat a bit with the typing in the following code. +// In reality, `expose` receives a function and returns it again. However, we tell flow that it wraps the function, so that +// unwrapping becomes necessary. +// The unwrapping has to be done with `createWorker` which in fact instantiates the worker class. +// As a result, we have some cheated types in the following two functions, but gain type safety for all usages of web worker modules. +type UseCreateWorkerToUseMe = { +_wrapped: T }; + +export function createWorker(WorkerClass: UseCreateWorkerToUseMe): T { + if (proxy == null) { + // In a node context (e.g., when executing tests), we don't create web workers which is why + // we can simply return the input function here. + // $FlowIgnore return WorkerClass; } @@ -36,15 +49,16 @@ export const createWorker: (fn: Fn) => Fn = WorkerClass => { // directly imported. We exploit this by simply typing this createWorker function as an identity function // (T => T). That way, we gain proper flow typing for functions executed in web workers. However, // we need to suppress the following flow error for that to work. - // $FlowFixMe + // $FlowIgnore new WorkerClass(), ); -}; +} -export const expose = (fn: T): T => { +export function expose(fn: T): UseCreateWorkerToUseMe { // In a node context (e.g., when executing tests), we don't create web workers - if (!isNodeContext) { + if (_expose != null) { _expose(fn, self); } + // $FlowIgnore return fn; -}; +} diff --git a/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js b/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js index dcbae54d674..e4a9c54e8e6 100644 --- a/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js +++ b/app/assets/javascripts/oxalis/workers/fetch_buffer.worker.js @@ -3,7 +3,7 @@ import handleStatus from "libs/handle_http_status"; import { expose } from "./comlink_wrapper"; -function fetchBufferViaWebworker(url: RequestInfo, options?: RequestOptions) { +function fetchBufferViaWebworker(url: RequestInfo, options?: RequestOptions): Promise { return fetch(url, options) .then(handleStatus) .then(response => response.arrayBuffer());