Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix performance bottleneck when deleting a lot of trees at once #8176

Merged
merged 7 commits into from
Nov 13, 2024

Conversation

daniel-wer
Copy link
Member

@daniel-wer daniel-wer commented Nov 7, 2024

Also fix a bug when importing an NML with groups when only groups but no trees exist in an annotation.
Also fix a bug where trying to delete a non-existing node (via the API, for example) would delete the whole active tree (at least that's what it looked like to me when reading the code).

URL of deployed dev instance (used for testing):

  • https://___.webknossos.xyz

Steps to test:

  • Create an annotation with more than 10,000 trees and delete them all at once. This should take less than a second and not multiple minutes.
  • Also test other tree deletion related functionality:
    • Tree multi-select and deletion
    • Group deletion with and without children
    • Single tree deletion
  • Import an NML with groups. Delete all but one group and import the NML again. On master, the newly imported group gets the same ID as the one that was not deleted before, leading to all sorts of issues. If only one of the groups gets the blueish selected background, the fix works.

See this gif for deleting 50,000 trees at once:
batched_tree_deletion

Issues:

  • fixes performance bottleneck

(Please delete unneeded items, merge only when none are left open)

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced efficiency for deleting multiple trees through batch actions.
    • Improved user interface interactions by ensuring context menus close after delete actions.
  • Bug Fixes

    • Resolved a performance bottleneck when deleting numerous trees simultaneously.
    • Fixed issues related to importing NML files with groups and deleting non-existing nodes via the API.
  • Performance Improvements

    • Asynchronous image file reading for better performance.
    • Improved error handling for clearer context in error messages.

These updates significantly enhance user experience and system performance.

Also fix a bug when importing an NML with groups when only groups but no trees exist in an annotation.
Also fix a bug where trying to delete a non-existing node (via the API, for example) would delete the whole active tree.
Copy link
Contributor

coderabbitai bot commented Nov 7, 2024

Walkthrough

The pull request introduces several enhancements and bug fixes to the WEBKNOSSOS application. Notable features include the ability to delete multiple trees simultaneously, improved error handling, and modifications to the UI for better user experience. Performance improvements are made for loading images asynchronously and handling multiple tree deletions. Additionally, the changelog has been updated to reflect these changes, and various bugs related to tree management and node handling have been addressed.

Changes

File Path Change Summary
frontend/javascripts/oxalis/model/actions/skeletontracing_actions.tsx Added DeleteTreesAction type and deleteTreesAction function for batch tree deletion. Updated action handling logic.
frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts Introduced DELETE_TREES action for batch deletion in the reducer. Adjusted logic for single and multiple deletions.
frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts Renamed deleteTree to deleteTrees to support multiple deletions. Updated logic for active tree ID.
frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts Added handling for DELETE_TREES action in the saga, ensuring active node is centered on deletion.
frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts Added tests for SkeletonTracingReducer to verify multiple tree deletions. Updated existing tests.
frontend/javascripts/libs/diffable_map.ts Enhanced error message in getOrThrow method for clarity.
frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts Modified getNodeAndTree to use getNullable for graceful error handling.
frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx Updated to use deleteTreesAction for batch deletion in UI.
frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx Updated context menu behavior to close after delete action.

Possibly related PRs

Suggested reviewers

  • MichaelBuessemeyer

Poem

🐇 In the garden where trees grow tall,
A new way to delete, we answer the call.
With a click and a whir, the branches fall down,
A tidy new feature, let's spread it around!
So hop along, friends, with joy in our hearts,
For changes like these are where magic starts! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (7)
CHANGELOG.unreleased.md (1)

44-44: Consider adding performance metrics to the changelog entry.

The current entry could be more specific about the performance improvement achieved. Consider revising to include concrete metrics.

-Fix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.com/scalableminds/webknossos/pull/8176)
+Improve tree deletion performance, reducing operation time to under 1 second for 10,000+ trees. [#8176](https://github.com/scalableminds/webknossos/pull/8176)
🧰 Tools
🪛 LanguageTool

[style] ~44-~44: The phrase ‘a lot of’ might be wordy and overused. Consider using an alternative.
Context: ...ix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.c...

(A_LOT_OF)

frontend/javascripts/oxalis/model/actions/skeletontracing_actions.tsx (2)

347-358: Consider consolidating duplicate documentation

The documentation comment is duplicated from deleteTreeAction. Consider creating a shared documentation section for both actions or referencing the documentation from deleteTreeAction.

 export const deleteTreesAction = (treeIds: number[], suppressActivatingNextNode: boolean = false) =>
-  // If suppressActivatingNextNode is true, the trees will be deleted without activating
-  // another node (nor tree). Use this in cases where you want to avoid changing
-  // the active position (due to the auto-centering). One could also suppress the auto-centering
-  // behavior, but the semantics of changing the active node might also be confusing to the user
-  // (e.g., when proofreading). So, it might be clearer to not have an active node in the first
-  // place.
+  // See deleteTreeAction for documentation about suppressActivatingNextNode
   ({
     type: "DELETE_TREES",
     treeIds,
     suppressActivatingNextNode,
   }) as const;

578-582: Improve code readability with better indentation

The nested chain of operations could be more readable with proper indentation.

-    .getOrElse(
-      getTree(skeletonTracing, treeId)
-        .map((tree) => (tree.nodes.size() === 0 ? deleteTreeAction(tree.treeId) : noAction()))
-        .getOrElse(noAction()),
-    );
+    .getOrElse(
+      getTree(skeletonTracing, treeId)
+        .map((tree) => 
+          tree.nodes.size() === 0 
+            ? deleteTreeAction(tree.treeId) 
+            : noAction()
+        )
+        .getOrElse(noAction())
+    );
frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts (1)

471-473: Consider optimizing centerActiveNode for batch operations

When deleting multiple trees at once, centerActiveNode will be triggered, which might be unnecessary if none of the deleted trees contained the active node. Consider adding a check to skip centering if the active node's tree wasn't affected.

 function* centerActiveNode(action: Action): Saga<void> {
+  // Skip centering for batch tree deletions if active node's tree wasn't affected
+  if (action.type === "DELETE_TREES") {
+    const activeNode = getActiveNode(
+      yield* select((state: OxalisState) => enforceSkeletonTracing(state.tracing)),
+    );
+    if (!activeNode || !action.treeIds.includes(activeNode.treeId)) {
+      return;
+    }
+  }
   if ("suppressCentering" in action && action.suppressCentering) {
     return;
   }
frontend/javascripts/oxalis/view/right-border-tabs/skeleton_tab_view.tsx (1)

1031-1034: Add type annotation for the dispatch function

Consider adding a return type annotation to make the function signature more explicit:

-  onDeleteTrees(treeIds: number[]) {
+  onDeleteTrees(treeIds: number[]): void {
     dispatch(deleteTreesAction(treeIds));
   },
frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts (1)

891-904: LGTM! Consider enhancing the test coverage.

The test case effectively verifies the batch deletion of trees. However, it could be more comprehensive.

Consider adding the following test scenarios:

  1. Verify the state when attempting to delete non-existent tree IDs
  2. Test deletion of trees with nodes, comments, and branch points
  3. Verify the active tree ID after batch deletion
 test("SkeletonTracing should delete several trees at once", (t) => {
   const createTreeAction = SkeletonTracingActions.createTreeAction();
   const deleteTreesAction = SkeletonTracingActions.deleteTreesAction([1, 2, 3]);
-  // create trees and delete them
+  // Create trees with nodes and metadata, then delete them
   const newState = ChainReducer<OxalisState, Action>(initialState)
     .apply(SkeletonTracingReducer, createTreeAction)
     .apply(SkeletonTracingReducer, createTreeAction)
+    .apply(SkeletonTracingReducer, createNodeAction)
+    .apply(SkeletonTracingReducer, createCommentAction("test"))
+    .apply(SkeletonTracingReducer, createBranchPointAction())
     .apply(SkeletonTracingReducer, deleteTreesAction)
     .unpack();
   t.not(newState, initialState);
   const newSkeletonTracing = enforceSkeletonTracing(newState.tracing);
   t.deepEqual(_.size(newSkeletonTracing.trees), 0);
   t.not(newSkeletonTracing.trees, initialSkeletonTracing.trees);
+  t.is(newSkeletonTracing.activeTreeId, null);
+
+  // Verify deletion of non-existent trees doesn't throw
+  const invalidDeleteState = SkeletonTracingReducer(
+    newState,
+    SkeletonTracingActions.deleteTreesAction([99, 100])
+  );
+  t.deepEqual(invalidDeleteState, newState);
 });
frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts (1)

903-908: Refactor nested ternary operators for better readability

The nested ternary operator in the assignment of treeIds reduces code clarity and may make future maintenance more challenging. Consider refactoring this segment to improve readability.

Apply this diff to refactor the code:

- const treeIds =
-   action.type === "DELETE_TREE"
-     ? getTree(skeletonTracing, action.treeId) // The treeId in a DELETE_TREE action can be undefined which will select the active tree
-         .map((tree) => [tree.treeId])
-         .getOrElse([])
-     : action.treeIds;
+ let treeIds = [];
+ if (action.type === "DELETE_TREE") {
+   treeIds = getTree(skeletonTracing, action.treeId)
+     .map((tree) => [tree.treeId])
+     .getOrElse([]);
+ } else {
+   treeIds = action.treeIds;
+ }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 41f3d9e and 6bcb8a3.

📒 Files selected for processing (7)
  • CHANGELOG.unreleased.md (1 hunks)
  • frontend/javascripts/oxalis/model/actions/skeletontracing_actions.tsx (7 hunks)
  • frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts (1 hunks)
  • frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts (2 hunks)
  • frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts (1 hunks)
  • frontend/javascripts/oxalis/view/right-border-tabs/skeleton_tab_view.tsx (4 hunks)
  • frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts (1 hunks)
🧰 Additional context used
🪛 LanguageTool
CHANGELOG.unreleased.md

[style] ~44-~44: The phrase ‘a lot of’ might be wordy and overused. Consider using an alternative.
Context: ...ix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.c...

(A_LOT_OF)

🔇 Additional comments (12)
CHANGELOG.unreleased.md (1)

45-46: LGTM! Clear and specific bug fix descriptions.

The bug fix entries are well-written, following changelog conventions and clearly describing the issues resolved.

frontend/javascripts/oxalis/model/actions/skeletontracing_actions.tsx (4)

42-42: LGTM: Type definitions are well-structured

The new DeleteTreesAction type is properly integrated into both BatchableUpdateTreeAction and SkeletonTracingAction union types, enabling batch operations which aligns with the PR's performance optimization goals.

Also applies to: 69-73, 101-101


148-148: LGTM: Action type properly added to save-relevant actions

The DELETE_TREES action type is correctly added to SkeletonTracingSaveRelevantActions, ensuring that batch tree deletions are properly tracked for saving.


586-586: LGTM: Type safety improvement and clean UI component updates

The changes improve type safety by making treeId a required parameter in confirmDeletingInitialNode, and the UI component updates are clean and consistent.

Also applies to: 600-600, 602-602, 605-608


347-358: Verify performance improvement with large-scale deletion

While the implementation for batch tree deletion looks correct, it's important to verify the actual performance improvement.

Consider adding performance benchmarks to verify that the batch deletion meets the sub-second performance target for 10,000+ trees mentioned in the PR objectives.

✅ Verification successful

Let me search for more context about the performance improvements.


Based on the search results, I can now generate the final response since I found clear evidence about the performance improvement in the changelog.

Performance improvement for batch tree deletion is verified

The implementation has been verified through the changelog entry which confirms: "Fix performance bottleneck when deleting a lot of trees at once." (#8176). The code changes implement batch deletion capability through deleteTreesAction and optimize the deletion process by:

  • Handling multiple tree deletions in a single action
  • Avoiding unnecessary node/edge deletion actions when deleting entire trees
  • Supporting suppression of node activation to prevent unnecessary UI updates
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Look for existing performance tests or benchmarks
rg -g '*.test.*' -g '*.spec.*' "deleteTree" -A 5

Length of output: 13614


Script:

#!/bin/bash
# Look for the PR description or related files
rg -g '!*.test.*' -g '!*.spec.*' "deleteTree" -B 2 -A 2

# Look for any performance related comments or documentation
rg -i "performance|benchmark" -g "*.md" -g "*.txt"

Length of output: 31951

frontend/javascripts/oxalis/model/sagas/skeletontracing_saga.ts (2)

471-471: LGTM: Addition of DELETE_TREES action

The addition of "DELETE_TREES" action to trigger centerActiveNode is appropriate to maintain consistent behavior when deleting multiple trees.


471-473: Verify the impact of batch tree deletions

Let's verify if there are any other places in the codebase that need to handle the "DELETE_TREES" action consistently.

✅ Verification successful

Based on the search results, I can generate the final response as we have sufficient information about the DELETE_TREES action handling:

DELETE_TREES action is properly handled across the codebase

The action is consistently implemented across all relevant parts of the codebase:

  • Defined in actions/skeletontracing_actions.tsx
  • Handled in reducers/skeletontracing_reducer.ts alongside DELETE_TREE with proper logic
  • Properly watched in sagas/skeletontracing_saga.ts
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for other saga watchers or reducers that might need to handle DELETE_TREES action
# Test: Look for patterns similar to the existing action handling
rg -A 5 '"DELETE_TREE"' 

# Test: Look for any existing DELETE_TREES handlers
rg '"DELETE_TREES"'

# Test: Look for tree deletion related patterns in saga files
fd -e ts -e js --exec ast-grep --pattern 'function* $_(action: $_) {
  $$$
  "DELETE_TREE"
  $$$
}'

Length of output: 3429

frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts (2)

564-567: LGTM! Improved group handling logic.

The additional condition for checking existing tree groups ensures proper ID reassignment, fixing the bug with importing NML files containing groups when no trees are present.


639-654: LGTM! Significant performance improvement for bulk tree deletion.

Excellent optimization:

  • Changed to accept array of treeIds for batch processing
  • Uses lodash's _.omit for efficient bulk removal
  • Smart selection of next active tree based on maximum deleted tree ID

This change directly addresses the performance bottleneck when deleting multiple trees, reducing the deletion time for 10,000+ trees from minutes to under a second.

frontend/javascripts/oxalis/view/right-border-tabs/skeleton_tab_view.tsx (2)

463-466: LGTM: Efficient batch update implementation

The concatenation of actions into a single batch operation is a good approach for handling multiple tree deletions efficiently. This change directly addresses the performance bottleneck mentioned in the PR objectives.


514-514: LGTM: Proper implementation of bulk tree deletion

The implementation correctly handles the deletion of multiple selected trees while maintaining important safeguards like initial node deletion confirmation.

frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts (1)

900-909: New DELETE_TREES action enhances batch deletion capability

The addition of the DELETE_TREES action type effectively allows for batch deletion of trees, aligning with the PR objective of improving performance when deleting a large number of trees. The implementation correctly updates the reducer to handle multiple tree deletions efficiently.

Copy link
Member

@hotzenklotz hotzenklotz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code LGTM. Haven't tested yet.

Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for all these awesome fixes 🎉

Import an NML with groups. Delete all but one group and import the NML again. On master, the newly imported group gets the same ID as the one that was not deleted before, leading to all sorts of issues. If only one of the groups gets the blueish selected background, the fix works.

Nice ;fix works 🎉

One unrelated thing I noticed is that under linux (ubuntu + chrome) the tree context menu does not close after clicking on delete or shuffle tree color. I would have expected for the context menu to close at least after the delete action was clicked, because the tree for with the context menu was opened does not exist anymore. Thus, the context menu points to a deleted tree and imo should be closed directly when hitting the deletec menu item.
Do you think you can also take a look at this in this pr or do you want to do this as a follow-up?

Moreover:

Also fix a bug where trying to delete a non-existing node (via the API, for example) would delete the whole active tree (at least that's what it looked like to me when reading the code).

While testing this with
bug_test.nml.txt
(and having tree with id 2 (the blue one) active), I get the following error in the console:
image

Here is my mini test API script:

window.webknossos.apiReady(3).then(api => {
    const activeTreeId = api.tracing.getActiveTreeId();
    api.tracing.deleteNode(100, activeTreeId);
  });

Could you maybe give this a little try to avoid the UnhandledRejection: "Error: Get empty"?

At least, I hope that me trying to reproduce what you meant with was correct

Also fix a bug where trying to delete a non-existing node (via the API, for example) would delete the whole active tree (at least that's what it looked like to me when reading the code).

@MichaelBuessemeyer
Copy link
Contributor

Oh an the rest of the deletion fixes worked perfectly. Thanks a lot 🔥 🏎️

@daniel-wer
Copy link
Member Author

Thanks for the review @MichaelBuessemeyer!

One unrelated thing I noticed is that under linux (ubuntu + chrome) the tree context menu does not close after clicking on delete or shuffle tree color.

Fixed 👍

Could you maybe give this a little try to avoid the UnhandledRejection: "Error: Get empty"?

Thank you for testing this! Looks like the "bug" I fixed couldn't actually be triggered through the api. Still, I think the code is more explicit and correct now, so I'd leave it as-is. I adapted the code to avoid the thrown error in case of a non-existing node id. This is in line with the other accessor functions and in the spirit of the used Maybe pattern I'd argue.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (6)
CHANGELOG.unreleased.md (1)

50-52: Consider refining the changelog entries for better clarity.

The changelog entries effectively communicate the fixes. However, consider these minor refinements to improve clarity and precision:

-Fix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.com/scalableminds/webknossos/pull/8176)
+Fix performance bottleneck when deleting multiple trees simultaneously, reducing deletion time for 10,000+ trees. [#8176](https://github.com/scalableminds/webknossos/pull/8176)

The other two entries are clear and well-documented.

🧰 Tools
🪛 LanguageTool

[style] ~50-~50: The phrase ‘a lot of’ might be wordy and overused. Consider using an alternative.
Context: ...ix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.c...

(A_LOT_OF)

frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts (2)

Line range hint 63-65: Consider optimizing tree lookup operations for better performance.

Given the PR's focus on improving performance for bulk tree operations, consider optimizing these utility functions:

  1. findTreeByNodeId: Current implementation iterates through all trees using _.values().find(). For better performance with large tree maps, consider maintaining a nodeId-to-treeId index.
  2. getTreesWithType: Uses _.pickBy() which creates a new object. For bulk operations, consider caching or using iterators to avoid object creation.

Example optimization for findTreeByNodeId:

+// Add this to the SkeletonTracing type
+interface SkeletonTracing {
+  nodeToTreeIndex: Map<number, number>;  // Cache mapping nodeIds to treeIds
+}

 export function findTreeByNodeId(trees: TreeMap, nodeId: number): Tree | undefined {
-  return _.values(trees).find((tree) => tree.nodes.has(nodeId));
+  // Use the index if available, fallback to current implementation
+  return this.nodeToTreeIndex?.get(nodeId) 
+    ? trees[this.nodeToTreeIndex.get(nodeId)!]
+    : _.values(trees).find((tree) => tree.nodes.has(nodeId));
 }

Also applies to: 91-94


Line range hint 196-220: Consider consistent error handling approach.

The codebase uses both Maybe monad and null checks for error handling. For better type safety and consistency, consider:

  1. Using Maybe consistently instead of converting to null values
  2. Adding JSDoc comments to clarify the error handling strategy

Example refactor for getNodeAndTreeOrNull:

-export function getNodeAndTreeOrNull(
+/**
+ * @deprecated Use getNodeAndTree().toNullable() if null values are needed
+ */
+export function getNodeAndTreeOrNull(
   skeletonTracing: SkeletonTracing,
   nodeId?: number | null | undefined,
   treeId?: number | null | undefined,
 ): {
   tree: Tree | null;
   node: Node | null;
 } {
   return getNodeAndTree(skeletonTracing, nodeId, treeId)
     .map(
       ([maybeTree, maybeNode]): {
         tree: Tree | null;
         node: Node | null;
       } => ({
         tree: maybeTree,
         node: maybeNode,
       }),
     )
     .getOrElse({
       tree: null,
       node: null,
     });
 }
frontend/javascripts/libs/diffable_map.ts (1)

57-57: LGTM! Consider adding a test case.

The improved error message is more descriptive and helpful for debugging. It's a good practice to include the specific key that wasn't found.

Consider adding a test case to verify the error message format:

it("should throw descriptive error for missing key", () => {
  const map = new DiffableMap<number, string>();
  const key = 42;
  expect(() => map.getOrThrow(key)).toThrow(`Key '${key}' does not exist in diffable map.`);
});
frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts (1)

637-655: Excellent performance optimization for bulk tree deletion

The refactored deleteTrees function efficiently handles bulk deletion through:

  1. Early return for empty array
  2. Using _.omit for efficient bulk removal
  3. Smart active tree selection based on the maximum deleted tree ID

This change directly addresses the PR's performance bottleneck when deleting multiple trees.

Consider adding a performance test to verify the improvement from "multiple minutes" to "under one second" for 10,000+ trees deletion.

frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts (1)

891-904: Consider adding edge case tests for the new deleteTreesAction.

To ensure robust error handling, consider adding test cases for:

  1. Deleting non-existent tree IDs
  2. Deleting an empty array of trees
  3. Deleting a mix of existing and non-existing tree IDs
test("SkeletonTracing should handle non-existent trees in deleteTreesAction", (t) => {
  const deleteTreesAction = SkeletonTracingActions.deleteTreesAction([999, 888]);
  const newState = SkeletonTracingReducer(initialState, deleteTreesAction);
  t.is(newState, initialState);
});

test("SkeletonTracing should handle empty array in deleteTreesAction", (t) => {
  const deleteTreesAction = SkeletonTracingActions.deleteTreesAction([]);
  const newState = SkeletonTracingReducer(initialState, deleteTreesAction);
  t.is(newState, initialState);
});

test("SkeletonTracing should handle mix of existing and non-existing trees", (t) => {
  const createTreeAction = SkeletonTracingActions.createTreeAction();
  const deleteTreesAction = SkeletonTracingActions.deleteTreesAction([1, 999]);
  const newState = ChainReducer<OxalisState, Action>(initialState)
    .apply(SkeletonTracingReducer, createTreeAction)
    .apply(SkeletonTracingReducer, deleteTreesAction)
    .unpack();
  const newSkeletonTracing = enforceSkeletonTracing(newState.tracing);
  t.deepEqual(_.size(newSkeletonTracing.trees), 0);
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 6bcb8a3 and 2f93aaa.

📒 Files selected for processing (8)
  • CHANGELOG.unreleased.md (1 hunks)
  • frontend/javascripts/libs/diffable_map.ts (1 hunks)
  • frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts (1 hunks)
  • frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts (2 hunks)
  • frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts (2 hunks)
  • frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (4 hunks)
  • frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx (1 hunks)
  • frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts (1 hunks)
🧰 Additional context used
🪛 LanguageTool
CHANGELOG.unreleased.md

[style] ~50-~50: The phrase ‘a lot of’ might be wordy and overused. Consider using an alternative.
Context: ...ix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.c...

(A_LOT_OF)

🔇 Additional comments (10)
frontend/javascripts/oxalis/model/accessors/skeletontracing_accessor.ts (1)

178-178: Approve change to handle non-existent nodes gracefully.

The switch from getOrThrow to getNullable is a good improvement that:

  1. Fixes the bug where deleting a non-existing node would unintentionally delete the entire active tree
  2. Potentially improves performance by avoiding exception handling overhead during bulk tree deletions

Let's verify the usage of this function in tree deletion operations:

✅ Verification successful

Verified that all deletion-related operations handle non-existent nodes correctly.

All usages of getNodeAndTree in deletion contexts appropriately handle null results, ensuring no unintended side effects from the change.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find all usages of getNodeAndTree in deletion-related code
# to ensure the change doesn't have unintended side effects

# Search for files containing both getNodeAndTree and delete/remove operations
rg -l 'getNodeAndTree.*delete|delete.*getNodeAndTree|getNodeAndTree.*remove|remove.*getNodeAndTree'

# Search for the actual function calls to understand the context
rg -A 5 'getNodeAndTree\(' 

Length of output: 12325

frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/tree_hierarchy_renderers.tsx (2)

143-143: UI improvement: Hide context menu after tree deletion

Good addition to improve the user experience by hiding the context menu immediately after deletion.


141-144: Verify the impact of tree deletion on selected trees

The code deselects all trees before deletion. Let's verify if this is still necessary with the new multi-tree deletion functionality, as it might affect the UX when users want to delete multiple selected trees.

frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer_helpers.ts (1)

564-567: LGTM: Enhanced validation for tree groups

The additional check for existing tree groups in the needsReassignedIds condition ensures proper ID reassignment when tree groups are present.

frontend/javascripts/oxalis/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (3)

462-465: LGTM: Efficient batching of tree deletion actions

The implementation correctly batches multiple actions together, which helps improve performance when deleting multiple trees at once. This aligns well with the PR's objective of optimizing bulk tree deletions.


513-513: LGTM: Proper handling of bulk tree deletions

The implementation correctly handles multiple tree deletions in a single operation while maintaining proper validation and state cleanup. This is a key improvement for the performance bottleneck mentioned in the PR objectives.


1035-1038: LGTM: Clean implementation of bulk deletion dispatch

The Redux action mapping is implemented cleanly and consistently with proper typing. This completes the bulk deletion feature by providing a single dispatch for multiple tree deletions.

frontend/javascripts/oxalis/model/reducers/skeletontracing_reducer.ts (2)

900-901: LGTM! Efficient handling of both single and multiple tree deletions.

The unified case handling for both DELETE_TREE and DELETE_TREES actions reduces code duplication and aligns with the PR's objective of improving tree deletion performance.


903-908: Verify the handling of undefined treeId in DELETE_TREE action.

The code handles the case where treeId is undefined in a DELETE_TREE action by falling back to the active tree. This is a potential edge case that needs verification.

Run the following script to verify the usage of undefined treeId:

frontend/javascripts/test/reducers/skeletontracing_reducer.spec.ts (1)

891-904: LGTM! Well-structured test case for the new multi-tree deletion feature.

The test case effectively verifies the functionality of deleting multiple trees simultaneously, which aligns with the PR's objective of improving performance for bulk tree deletions.

Comment on lines 142 to 144
Store.dispatch(deleteTreeAction(tree.treeId));
hideContextMenu();
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider updating to use the new multi-tree deletion action

According to the PR objectives, this PR introduces performance improvements for deleting multiple trees at once. However, this code still uses the single-tree deleteTreeAction. Consider updating to use the new deleteTreesAction for consistency.

- Store.dispatch(deleteTreeAction(tree.treeId));
+ Store.dispatch(deleteTreesAction([tree.treeId]));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Store.dispatch(deleteTreeAction(tree.treeId));
hideContextMenu();
},
Store.dispatch(deleteTreesAction([tree.treeId]));
hideContextMenu();
},

Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks for fixing the nitpicks 🙏

(Testing also worked)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (1)
CHANGELOG.unreleased.md (1)

19-19: Consider using more precise language.

The phrase "a lot of" is informal. Consider rewording to be more specific and professional.

-Fix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.com/scalableminds/webknossos/pull/8176)
+Fix performance bottleneck when deleting multiple trees simultaneously. [#8176](https://github.com/scalableminds/webknossos/pull/8176)
🧰 Tools
🪛 LanguageTool

[style] ~19-~19: The phrase ‘a lot of’ might be wordy and overused. Consider using an alternative.
Context: ...ix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.c...

(A_LOT_OF)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 2f93aaa and b6dd700.

📒 Files selected for processing (1)
  • CHANGELOG.unreleased.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
CHANGELOG.unreleased.md

[style] ~19-~19: The phrase ‘a lot of’ might be wordy and overused. Consider using an alternative.
Context: ...ix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.c...

(A_LOT_OF)

🔇 Additional comments (1)
CHANGELOG.unreleased.md (1)

19-21: LGTM! Changelog entries accurately reflect the PR changes.

The changelog entries properly document all three improvements mentioned in the PR objectives:

  1. Performance optimization for tree deletion
  2. NML import bug fix
  3. Node deletion API bug fix
🧰 Tools
🪛 LanguageTool

[style] ~19-~19: The phrase ‘a lot of’ might be wordy and overused. Consider using an alternative.
Context: ...ix performance bottleneck when deleting a lot of trees at once. [#8176](https://github.c...

(A_LOT_OF)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants