Skip to content

Commit a0bf300

Browse files
authored
Speed up JSON mappings (#7706)
* Use Map instead of Object for JSON mappings. Also, no longer maintain extra mappingKeys array. * update changelog * Keep frontend api backwards compatible * Merge branch 'master' into use-map-for-mappings
1 parent ee83493 commit a0bf300

File tree

14 files changed

+48
-75
lines changed

14 files changed

+48
-75
lines changed

CHANGELOG.unreleased.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
2020
- If storage scan is enabled, the measured used storage is now displayed in the dashboard’s dataset detail view. [#7677](https://github.com/scalableminds/webknossos/pull/7677)
2121
- Prepared support to download full stl meshes via the HTTP api. [#7587](https://github.com/scalableminds/webknossos/pull/7587)
2222
- You can now place segment index files with your on-disk segmentation layers, which makes segment stats available when viewing these segmentations, and also when working on volume annotations based on these segmentation layers. [#7437](https://github.com/scalableminds/webknossos/pull/7437)
23-
- Added an action to delete erronous, unimported datasets directly from the dashboard. [#7448](https://github.com/scalableminds/webknossos/pull/7448)
23+
- Added an action to delete erroneous, unimported datasets directly from the dashboard. [#7448](https://github.com/scalableminds/webknossos/pull/7448)
2424
- Added support for `window`, `active`, `inverted` keys from the `omero` info in the NGFF metadata. [7685](https://github.com/scalableminds/webknossos/pull/7685)
2525
- Added getSegment function to JavaScript API. Also, createSegmentGroup returns the id of the new group now. [#7694](https://github.com/scalableminds/webknossos/pull/7694)
2626
- Added support for importing N5 datasets without multiscales metadata. [#7664](https://github.com/scalableminds/webknossos/pull/7664)
@@ -37,6 +37,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
3737
- If the current dataset folder in the dashboard cannot be found (e.g., because somebody else deleted it), the page navigates to the root folder automatically. [#7669](https://github.com/scalableminds/webknossos/pull/7669)
3838
- Voxelytics logs are now stored by organization name, rather than id, in Loki. This is in preparation of the unification of these two concepts. [#7687](https://github.com/scalableminds/webknossos/pull/7687)
3939
- Using a segment index file with a different data type than uint16 will now result in an error. [#7698](https://github.com/scalableminds/webknossos/pull/7698)
40+
- Improved performance of JSON mappings in preparation of frontend HDF5 mappings. [#7706](https://github.com/scalableminds/webknossos/pull/7706)
4041

4142
### Fixed
4243
- Fixed rare SIGBUS crashes of the datastore module that were caused by memory mapping on unstable file systems. [#7528](https://github.com/scalableminds/webknossos/pull/7528)

frontend/javascripts/oxalis/api/api_latest.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,7 +1497,7 @@ class DataApi {
14971497
*/
14981498
setMapping(
14991499
layerName: string,
1500-
mapping: Mapping,
1500+
mapping: Mapping | Record<number, number>,
15011501
options: {
15021502
colors?: Array<number>;
15031503
hideUnmappedIds?: boolean;
@@ -1517,10 +1517,10 @@ class DataApi {
15171517
sendAnalyticsEvent("setMapping called with custom colors");
15181518
}
15191519
const mappingProperties = {
1520-
mapping: _.clone(mapping),
1521-
// Object.keys is sorted for numerical keys according to the spec:
1522-
// http://www.ecma-international.org/ecma-262/6.0/#sec-ordinary-object-internal-methods-and-internal-slots-ownpropertykeys
1523-
mappingKeys: Object.keys(mapping).map((x) => parseInt(x, 10)),
1520+
mapping:
1521+
mapping instanceof Map
1522+
? new Map(mapping)
1523+
: new Map(Object.entries(mapping).map(([key, value]) => [parseInt(key, 10), value])),
15241524
mappingColors,
15251525
hideUnmappedIds,
15261526
showLoadingIndicator,

frontend/javascripts/oxalis/api/api_v2.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ class DataApi {
555555
*
556556
* api.setMapping("segmentation", mapping);
557557
*/
558-
setMapping(layerName: string, mapping: Mapping) {
558+
setMapping(layerName: string, mapping: Mapping | Record<number, number>) {
559559
const segmentationLayer = this.model.getLayerByName(layerName);
560560
const segmentationLayerName = segmentationLayer != null ? segmentationLayer.name : null;
561561

@@ -564,8 +564,10 @@ class DataApi {
564564
}
565565

566566
const mappingProperties = {
567-
mapping: _.clone(mapping),
568-
mappingKeys: Object.keys(mapping).map((x) => parseInt(x, 10)),
567+
mapping:
568+
mapping instanceof Map
569+
? new Map(mapping)
570+
: new Map(Object.entries(mapping).map(([key, value]) => [parseInt(key, 10), value])),
569571
};
570572
Store.dispatch(setMappingAction(layerName, "<custom mapping>", "JSON", mappingProperties));
571573
}

frontend/javascripts/oxalis/merger_mode.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { type AdditionalCoordinate } from "types/api_flow_types";
2626

2727
type MergerModeState = {
2828
treeIdToRepresentativeSegmentId: Record<number, number | null | undefined>;
29-
idMapping: Record<number, number>;
29+
idMapping: Map<number, number>;
3030
nodesPerSegment: Record<number, number>;
3131
nodes: Array<NodeWithTreeId>;
3232
// A properly initialized merger mode should always
@@ -48,7 +48,7 @@ function mapSegmentToRepresentative(
4848
mergerModeState: MergerModeState,
4949
) {
5050
const representative = getRepresentativeForTree(treeId, segId, mergerModeState);
51-
mergerModeState.idMapping[segId] = representative;
51+
mergerModeState.idMapping.set(segId, representative);
5252
}
5353

5454
function getRepresentativeForTree(treeId: number, segId: number, mergerModeState: MergerModeState) {
@@ -66,7 +66,7 @@ function getRepresentativeForTree(treeId: number, segId: number, mergerModeState
6666

6767
function deleteIdMappingOfSegment(segId: number, treeId: number, mergerModeState: MergerModeState) {
6868
// Remove segment from color mapping
69-
delete mergerModeState.idMapping[segId];
69+
mergerModeState.idMapping.delete(segId);
7070
delete mergerModeState.treeIdToRepresentativeSegmentId[treeId];
7171
}
7272

@@ -420,9 +420,9 @@ function resetState(mergerModeState: Partial<MergerModeState> = {}) {
420420
const state = Store.getState();
421421
const visibleLayer = getVisibleSegmentationLayer(state);
422422
const segmentationLayerName = visibleLayer != null ? visibleLayer.name : null;
423-
const defaults = {
423+
const defaults: MergerModeState = {
424424
treeIdToRepresentativeSegmentId: {},
425-
idMapping: {},
425+
idMapping: new Map(),
426426
nodesPerSegment: {},
427427
nodes: getAllNodesWithTreeId(),
428428
segmentationLayerName,

frontend/javascripts/oxalis/model/accessors/dataset_accessor.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,6 @@ export function is2dDataset(dataset: APIDataset): boolean {
655655
const dummyMapping = {
656656
mappingName: null,
657657
mapping: null,
658-
mappingKeys: null,
659658
mappingColors: null,
660659
hideUnmappedIds: false,
661660
mappingStatus: MappingStatusEnum.DISABLED,

frontend/javascripts/oxalis/model/actions/settings_actions.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ export const setMappingEnabledAction = (layerName: string, isMappingEnabled: boo
174174

175175
export type OptionalMappingProperties = {
176176
mapping?: Mapping;
177-
mappingKeys?: Array<number>;
178177
mappingColors?: Array<number>;
179178
hideUnmappedIds?: boolean;
180179
showLoadingIndicator?: boolean;
@@ -183,21 +182,14 @@ export const setMappingAction = (
183182
layerName: string,
184183
mappingName: string | null | undefined,
185184
mappingType: MappingType = "JSON",
186-
{
187-
mapping,
188-
mappingKeys,
189-
mappingColors,
190-
hideUnmappedIds,
191-
showLoadingIndicator,
192-
}: OptionalMappingProperties = {},
185+
{ mapping, mappingColors, hideUnmappedIds, showLoadingIndicator }: OptionalMappingProperties = {},
193186
) =>
194187
({
195188
type: "SET_MAPPING",
196189
layerName,
197190
mappingName,
198191
mappingType,
199192
mapping,
200-
mappingKeys,
201193
mappingColors,
202194
hideUnmappedIds,
203195
showLoadingIndicator,

frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ class DataCube {
175175
const mapping = this.getMapping();
176176

177177
if (mapping != null && this.isMappingEnabled()) {
178-
mappedId = mapping[idToMap];
178+
mappedId = mapping.get(idToMap);
179179
}
180180

181181
if (this.shouldHideUnmappedIds() && mappedId == null) {
@@ -841,7 +841,7 @@ class DataCube {
841841
const dataValue = Number(data[voxelIndex]);
842842

843843
if (mapping) {
844-
const mappedValue = mapping[dataValue];
844+
const mappedValue = mapping.get(dataValue);
845845

846846
if (mappedValue != null) {
847847
return mappedValue;

frontend/javascripts/oxalis/model/bucket_data_handling/mappings.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,30 +50,26 @@ class Mappings {
5050
(state) =>
5151
getMappingInfo(state.temporaryConfiguration.activeMappingByLayer, this.layerName).mapping,
5252
(mapping) => {
53-
const { mappingKeys } = getMappingInfo(
54-
Store.getState().temporaryConfiguration.activeMappingByLayer,
55-
this.layerName,
56-
);
57-
this.updateMappingTextures(mapping, mappingKeys);
53+
this.updateMappingTextures(mapping);
5854
},
5955
true,
6056
);
6157
}
6258

63-
async updateMappingTextures(
64-
mapping: Mapping | null | undefined,
65-
mappingKeys: Array<number> | null | undefined,
66-
): Promise<void> {
67-
if (mapping == null || mappingKeys == null) return;
59+
async updateMappingTextures(mapping: Mapping | null | undefined): Promise<void> {
60+
if (mapping == null) return;
6861
console.time("Time to create mapping texture");
69-
const mappingSize = mappingKeys.length;
62+
const mappingSize = mapping.size;
7063
// The typed arrays need to be padded with 0s so that their length is a multiple of MAPPING_TEXTURE_WIDTH
7164
const paddedLength =
7265
mappingSize + MAPPING_TEXTURE_WIDTH - (mappingSize % MAPPING_TEXTURE_WIDTH);
7366
const keys = new Uint32Array(paddedLength);
7467
const values = new Uint32Array(paddedLength);
68+
const mappingKeys = Array.from(mapping.keys());
69+
mappingKeys.sort((a, b) => a - b);
7570
keys.set(mappingKeys);
76-
values.set(mappingKeys.map((key) => mapping[key]));
71+
// @ts-ignore mappingKeys are guaranteed to exist in mapping as they are mapping.keys()
72+
values.set(mappingKeys.map((key) => mapping.get(key)));
7773
// Instantiate the Uint8Arrays with the array buffer from the Uint32Arrays, so that each 32-bit value is converted
7874
// to four 8-bit values correctly
7975
const uint8Keys = new Uint8Array(keys.buffer);

frontend/javascripts/oxalis/model/reducers/dataset_reducer.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ function DatasetReducer(state: OxalisState, action: Action): OxalisState {
4343
activeMappingByLayer: createDictWithKeysAndValue(segmentationLayerNames, () => ({
4444
mappingName: null,
4545
mapping: null,
46-
mappingKeys: null,
4746
mappingColors: null,
4847
hideUnmappedIds: false,
4948
mappingStatus: MappingStatusEnum.DISABLED,

frontend/javascripts/oxalis/model/reducers/settings_reducer.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ function SettingsReducer(state: OxalisState, action: Action): OxalisState {
250250
}
251251

252252
case "SET_MAPPING": {
253-
const { mappingName, mapping, mappingKeys, mappingColors, mappingType, layerName } = action;
253+
const { mappingName, mapping, mappingColors, mappingType, layerName } = action;
254254

255255
// Editable mappings cannot be disabled or switched for now
256256
if (!isMappingActivationAllowed(state, mappingName, layerName)) return state;
@@ -265,10 +265,9 @@ function SettingsReducer(state: OxalisState, action: Action): OxalisState {
265265
{
266266
mappingName,
267267
mapping,
268-
mappingKeys,
269268
mappingColors,
270269
mappingType,
271-
mappingSize: mappingKeys != null ? mappingKeys.length : 0,
270+
mappingSize: mapping != null ? mapping.size : 0,
272271
hideUnmappedIds,
273272
mappingStatus:
274273
mappingName != null ? MappingStatusEnum.ACTIVATING : MappingStatusEnum.DISABLED,

frontend/javascripts/oxalis/model/sagas/mapping_saga.ts

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -229,14 +229,9 @@ function* handleSetMapping(
229229
const fetchedMapping = fetchedMappings[mappingName];
230230
const { hideUnmappedIds, colors: mappingColors } = fetchedMapping;
231231

232-
const [mappingObject, mappingKeys] = yield* call(
233-
buildMappingObject,
234-
mappingName,
235-
fetchedMappings,
236-
);
232+
const mapping = yield* call(buildMappingObject, mappingName, fetchedMappings);
237233
const mappingProperties = {
238-
mapping: mappingObject,
239-
mappingKeys,
234+
mapping,
240235
mappingColors,
241236
hideUnmappedIds,
242237
};
@@ -259,9 +254,9 @@ function* handleSetMapping(
259254

260255
function convertMappingObjectToClasses(existingMapping: Mapping) {
261256
const classesByRepresentative: Record<number, number[]> = {};
262-
for (const unmappedStr of Object.keys(existingMapping)) {
263-
const unmapped = Number(unmappedStr);
264-
const mapped = existingMapping[unmapped];
257+
for (const unmapped of existingMapping.keys()) {
258+
// @ts-ignore unmapped is guaranteed to exist in existingMapping as it was obtained using existingMapping.keys()
259+
const mapped: number = existingMapping.get(unmapped);
265260
classesByRepresentative[mapped] = classesByRepresentative[mapped] || [];
266261
classesByRepresentative[mapped].push(unmapped);
267262
}
@@ -280,10 +275,10 @@ function* setCustomColors(
280275
let classIdx = 0;
281276
for (const aClass of classes) {
282277
const firstIdEntry = aClass[0];
283-
if (firstIdEntry == null) {
284-
continue;
285-
}
286-
const representativeId = mappingProperties.mapping[firstIdEntry];
278+
if (firstIdEntry == null) continue;
279+
280+
const representativeId = mappingProperties.mapping.get(firstIdEntry);
281+
if (representativeId == null) continue;
287282

288283
const hueValue = mappingProperties.mappingColors[classIdx];
289284
const color = jsHsv2rgb(360 * hueValue, 1, 1);
@@ -319,14 +314,8 @@ function* fetchMappings(
319314
}
320315
}
321316

322-
function buildMappingObject(
323-
mappingName: string,
324-
fetchedMappings: APIMappings,
325-
): [Mapping, Array<number>] {
326-
const mappingObject: Mapping = {};
327-
// Performance optimization: Object.keys(...) is slow for large objects
328-
// keeping track of the keys in a separate array is ~5x faster
329-
const mappingKeys = [];
317+
function buildMappingObject(mappingName: string, fetchedMappings: APIMappings): Mapping {
318+
const mappingObject: Mapping = new Map();
330319

331320
for (const currentMappingName of getMappingChain(mappingName, fetchedMappings)) {
332321
const mapping = fetchedMappings[currentMappingName];
@@ -341,17 +330,15 @@ function buildMappingObject(
341330
// The class is empty and can be ignored
342331
continue;
343332
}
344-
const mappedId = mappingObject[minId] || minId;
333+
const mappedId = mappingObject.get(minId) || minId;
345334

346335
for (const id of mappingClass) {
347-
mappingObject[id] = mappedId;
348-
mappingKeys.push(id);
336+
mappingObject.set(id, mappedId);
349337
}
350338
}
351339
}
352340

353-
mappingKeys.sort((a, b) => a - b);
354-
return [mappingObject, mappingKeys];
341+
return mappingObject;
355342
}
356343

357344
function getMappingChain(mappingName: string, fetchedMappings: APIMappings): Array<string> {

frontend/javascripts/oxalis/store.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,12 +389,11 @@ export type RecommendedConfiguration = Partial<
389389
// A histogram value of undefined indicates that the histogram hasn't been fetched yet
390390
// whereas a value of null indicates that the histogram couldn't be fetched
391391
export type HistogramDataForAllLayers = Record<string, APIHistogramData | null>;
392-
export type Mapping = Record<number, number>;
392+
export type Mapping = Map<number, number>;
393393
export type MappingType = "JSON" | "HDF5";
394394
export type ActiveMappingInfo = {
395395
readonly mappingName: string | null | undefined;
396396
readonly mapping: Mapping | null | undefined;
397-
readonly mappingKeys: number[] | null | undefined;
398397
readonly mappingColors: number[] | null | undefined;
399398
readonly hideUnmappedIds: boolean;
400399
readonly mappingStatus: MappingStatus;

frontend/javascripts/test/model/binary/cube.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ test("getDataValue() should return the mapping value if available", async (t) =>
200200
const { cube } = t.context;
201201
await cube._labelVoxelInResolution_DEPRECATED([0, 0, 0], null, 42, 0, null);
202202
await cube._labelVoxelInResolution_DEPRECATED([1, 1, 1], null, 43, 0, null);
203-
const mapping = [];
204-
mapping[42] = 1;
203+
const mapping = new Map();
204+
mapping.set(42, 1);
205205
t.is(cube.getDataValue([0, 0, 0], null, mapping), 1);
206206
t.is(cube.getDataValue([1, 1, 1], null, mapping), 43);
207207
});

frontend/javascripts/test/sagas/volumetracing/volumetracing_saga.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,7 @@ const initialState = update(defaultState, {
120120

121121
const dummyActiveMapping: ActiveMappingInfo = {
122122
mappingName: "dummy-mapping-name",
123-
mapping: {},
124-
mappingKeys: [],
123+
mapping: new Map(),
125124
mappingColors: [],
126125
hideUnmappedIds: false,
127126
mappingStatus: "ENABLED",

0 commit comments

Comments
 (0)