Skip to content

Reduce worker_manager memory allocation#4435

Open
tddang-linagora wants to merge 3 commits intomasterfrom
enhancement/worker-manager-memory-allocation
Open

Reduce worker_manager memory allocation#4435
tddang-linagora wants to merge 3 commits intomasterfrom
enhancement/worker-manager-memory-allocation

Conversation

@tddang-linagora
Copy link
Copy Markdown
Collaborator

@tddang-linagora tddang-linagora commented Apr 3, 2026

Issue

worker_manager is creating a lot of idle forever-living isolations without providing a way to dispose any of them, making the memory allocation of mobile is really high, even when those isolations are not in use.

Solution

Upgrade worker_manager to latest version, which provides the ability to create an isolation when needed and dispose when done.

Resolved

Mobile's memory usage is reduced by ~2/3. No improvement/negative impact for web

Platform Before After
Android android-before android-after
iOS ios-before ios-after
Web web-before web-after

Summary by CodeRabbit

  • Refactor

    • Switched background task execution to a unified worker manager, standardizing progress updates and main-isolate fallbacks (including single‑processor environments) for mail, thread, and file upload flows.
  • Bug Fixes

    • Updated worker dependency to address a worker initialization memory issue.
  • Chores

    • Simplified initialization and dependency wiring for background workers.
  • Tests

    • Simplified test setup by removing obsolete executor mocks.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8ec61917-39e8-4f33-977b-6329a615b369

📥 Commits

Reviewing files that changed from the base of the PR and between e6fdce1 and 9943d45.

📒 Files selected for processing (4)
  • lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart
  • lib/features/mailbox/data/network/mailbox_isolate_worker.dart
  • lib/features/thread/data/network/thread_isolate_worker.dart
  • lib/features/upload/data/network/file_uploader.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/features/mailbox/data/network/mailbox_isolate_worker.dart

Walkthrough

Refactors remove the injected Executor and replace its usage with workerManager.executeWithPort(...) across MailboxIsolateWorker, ThreadIsolateWorker, and FileUploader. Constructors no longer accept the executor. Isolate entry signatures changed from TypeSendPort/executor-based APIs to SendPort/executeWithPort closures, with new closure builders and helper functions. Progress reporting now uses onMessage values (ints) instead of executor notification payloads (lists). DI and app init were updated to stop registering/warming an Executor and to call workerManager.init(...); the worker_manager dependency was switched to a Git ref.

Suggested reviewers

  • hoangdat
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective of the pull request: reducing worker_manager memory allocation by migrating from long-lived idle isolates to on-demand isolate creation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch enhancement/worker-manager-memory-allocation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@chibenwa
Copy link
Copy Markdown
Member

chibenwa commented Apr 3, 2026

Great change proposal!

Copy link
Copy Markdown
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: 2

🧹 Nitpick comments (1)
pubspec.yaml (1)

180-184: Pin the temporary worker_manager fork to an immutable commit.

A branch ref can drift on the next dependency resolve, so a clean flutter pub get is not guaranteed to use the same code that produced the memory profile in this PR. Please switch ref to a commit SHA and verify pubspec.lock captures that exact revision.

♻️ Suggested change
   worker_manager:
     git:
       url: https://github.com/linagora/worker_manager.git
-      ref: hotfix/worker-init-memory-leak
+      ref: <commit-sha>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pubspec.yaml` around lines 180 - 184, Replace the mutable branch ref for the
worker_manager dependency with the specific commit SHA that was used to produce
the profile: change the worker_manager git ref from
hotfix/worker-init-memory-leak to the immutable commit hash (use the commit SHA
from the upstream fork), run flutter pub get to update dependencies, and verify
pubspec.lock records that exact git revision for the worker_manager entry;
ensure you update the ref value under the worker_manager git block in
pubspec.yaml and confirm pubspec.lock shows the same SHA.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/features/mailbox/data/network/mailbox_isolate_worker.dart`:
- Around line 73-82: markAsMailboxRead currently sends the full List<EmailId> on
each progress update, causing costly cross-isolate copies; change it to send
just an int count. Update the call to workerManager.executeWithPort to use
generics <List<EmailId>, int> (or the equivalent signature expecting an int
progress) and replace the onMessage handler to treat the incoming value as an
int and pass that into UpdatingMarkAsMailboxReadState as countRead; likewise
update the worker entrypoint (_handleMarkAsMailboxReadAction) to emit the
progress as an int (accumulated count) instead of sending the List<EmailId>.
Ensure references to markAsMailboxRead, _handleMarkAsMailboxReadAction,
onProgressController, and UpdatingMarkAsMailboxReadState are adjusted to use the
int progress pattern consistent with moveFolderContent.

In `@lib/features/thread/data/network/thread_isolate_worker.dart`:
- Around line 59-67: The current executeWithPort call is serializing the full
List<EmailId> back to the UI isolate each batch; change the protocol to send
only the processed count: update the generic type arguments on
workerManager.executeWithPort from List<EmailId>, List<EmailId> to
List<EmailId>, int (or simply int as the return payload), modify the worker
entrypoint _emptyMailboxFolderAction to send the batch count (an int) over the
SendPort instead of the accumulated List<EmailId>, and update the onMessage
handler to accept an int (use the value directly as processed count) and keep
emitting EmptyingFolderState(mailboxId, value, totalEmails). Ensure any type
annotations and return expectations for ThreadIsolateWorker::emptyMailboxFolder
/ _emptyMailboxFolderAction are adjusted accordingly.

---

Nitpick comments:
In `@pubspec.yaml`:
- Around line 180-184: Replace the mutable branch ref for the worker_manager
dependency with the specific commit SHA that was used to produce the profile:
change the worker_manager git ref from hotfix/worker-init-memory-leak to the
immutable commit hash (use the commit SHA from the upstream fork), run flutter
pub get to update dependencies, and verify pubspec.lock records that exact git
revision for the worker_manager entry; ensure you update the ref value under the
worker_manager git block in pubspec.yaml and confirm pubspec.lock shows the same
SHA.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bd83ebcf-b31e-4bf5-9bd7-0422dc54b9d5

📥 Commits

Reviewing files that changed from the base of the PR and between 6e285c4 and 6833140.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • lib/features/mailbox/data/network/mailbox_isolate_worker.dart
  • lib/features/thread/data/network/thread_isolate_worker.dart
  • lib/features/upload/data/network/file_uploader.dart
  • lib/main/bindings/network/network_bindings.dart
  • lib/main/bindings/network/network_isolate_binding.dart
  • lib/main/main_entry.dart
  • pubspec.yaml
  • test/features/identity_creator/presentation/identity_creator_controller_test.dart
💤 Files with no reviewable changes (3)
  • lib/main/bindings/network/network_bindings.dart
  • lib/main/bindings/network/network_isolate_binding.dart
  • test/features/identity_creator/presentation/identity_creator_controller_test.dart

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 3, 2026

This PR has been deployed to https://linagora.github.io/tmail-flutter/4435.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/features/upload/data/network/file_uploader.dart`:
- Around line 64-76: The non-web worker path doesn't propagate the cancelToken
so uploads can't be aborted; propagate the cancelToken into the isolate flow by
adding it to UploadFileArguments (used by _buildUploadClosure) and then use that
token inside the worker handler (_handleUploadAttachmentAction) when calling
_dioClient.post so the request is cancellable; if your worker/executeWithPort
API cannot cancel running isolates, detect a non-null cancelToken and run the
upload on the main isolate instead (i.e., avoid the worker.path) so the existing
cancelToken-based cancellation logic is honored.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ec2c4637-36e4-4146-b4b5-51e2aec807c3

📥 Commits

Reviewing files that changed from the base of the PR and between 6833140 and e6fdce1.

📒 Files selected for processing (3)
  • lib/features/mailbox/data/network/mailbox_isolate_worker.dart
  • lib/features/thread/data/network/thread_isolate_worker.dart
  • lib/features/upload/data/network/file_uploader.dart

Comment on lines +64 to +76
final args = UploadFileArguments(
_dioClient,
_fileUtils,
uploadId,
fileInfo,
uploadUri,
rootIsolateToken,
);
return await worker.workerManager.executeWithPort<Attachment, Success>(
_buildUploadClosure(args),
onMessage: (value) {
log('FileUploader::uploadAttachment(): onUpdateProgress: $value');
onSendController?.add(Right(value));
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 3, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n[1] FileUploader cancelToken usage\n'
rg -n -C2 --type=dart '\bcancelToken\b|executeWithPort|dioClient\.post\(' lib/features/upload/data/network/file_uploader.dart

printf '\n[2] Upload flows that pass or observe cancellation\n'
rg -n -C2 --type=dart 'uploadAttachment\(|CancelToken|isCancelled|\.cancel\(' lib/features/upload

Repository: linagora/tmail-flutter

Length of output: 8102


Non-web uploads ignore cancelToken in the isolate-backed request flow.

The method accepts cancelToken (line 46) and the web branch passes it to _dioClient.post() (line 189), but the non-web worker path does not. At line 110, _handleUploadAttachmentAction() calls dioClient.post() without a cancellation token. This allows callers to cancel uploads and update local state while the HTTP request continues in the worker isolate and completes on the server.

Options: implement cancellation via a dedicated worker-cancel channel or fall back to the main isolate for cancellable uploads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/features/upload/data/network/file_uploader.dart` around lines 64 - 76,
The non-web worker path doesn't propagate the cancelToken so uploads can't be
aborted; propagate the cancelToken into the isolate flow by adding it to
UploadFileArguments (used by _buildUploadClosure) and then use that token inside
the worker handler (_handleUploadAttachmentAction) when calling _dioClient.post
so the request is cancellable; if your worker/executeWithPort API cannot cancel
running isolates, detect a non-null cancelToken and run the upload on the main
isolate instead (i.e., avoid the worker.path) so the existing cancelToken-based
cancellation logic is honored.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Implement a dedicated worker-cancel channel is costly, and fall back to main isolate just for the cancel capability affect the main isolate performance.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@tddang-linagora, that's a fair trade-off given the substantial memory gains demonstrated in the profiling images (Android Dart/Flutter heap drops from ~375 MB to ~109 MB, iOS from ~297 MB to ~105 MB). Introducing cross-isolate cancellation plumbing or forcing the main isolate to handle uploads just to support cancellation would offset those wins.

A minimal mitigation would be adding a short comment near the executeWithPort call so future maintainers understand the deliberate limitation:

// Note: cancelToken is not propagated into the worker isolate.
// Cross-isolate cancellation is not supported here due to cost/complexity;
// cancelling the upload task via the caller will not abort the in-flight HTTP request.
return await worker.workerManager.executeWithPort<Attachment, Success>(
  _buildUploadClosure(args),
  onMessage: (value) {
    log('FileUploader::uploadAttachment(): onUpdateProgress: $value');
    onSendController?.add(Right(value));
  },
)

This keeps the code self-documenting without adding any runtime overhead. Up to you whether you want to add it.


✏️ Learnings added
Learnt from: tddang-linagora
Repo: linagora/tmail-flutter PR: 4435
File: lib/features/upload/data/network/file_uploader.dart:64-76
Timestamp: 2026-04-07T03:15:49.896Z
Learning: In linagora/tmail-flutter, the non-web upload path in `lib/features/upload/data/network/file_uploader.dart` deliberately does NOT propagate `cancelToken` into the `worker.workerManager.executeWithPort` isolate call. This is an accepted trade-off: implementing a worker-cancel channel was considered too costly, and falling back to the main isolate for cancellable uploads was rejected due to main-isolate performance impact. Do not flag the missing cancelToken propagation as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: dab246
Repo: linagora/tmail-flutter PR: 4362
File: lib/features/login/domain/exceptions/authentication_exception.dart:54-62
Timestamp: 2026-03-09T03:46:24.006Z
Learning: In tmail-flutter, `DownloadAttachmentHasTokenExpiredException` (lib/features/login/domain/exceptions/authentication_exception.dart) intentionally does NOT override `props` to include `refreshToken`, for security reasons — the token must not be leaked via equality checks, debug output, or crash reporters like Sentry.

Learnt from: dab246
Repo: linagora/tmail-flutter PR: 4422
File: lib/features/mailbox_dashboard/presentation/mixin/setup_preferences_setting_mixin.dart:24-30
Timestamp: 2026-03-27T11:00:56.451Z
Learning: In linagora/tmail-flutter, the `collapseThreads` getter in `lib/features/mailbox_dashboard/presentation/mixin/setup_preferences_setting_mixin.dart` does NOT require a server Session capability check. It is enabled purely based on two conditions: (1) the in-app thread setting is enabled (`isThreadDetailEnabled == true`) and (2) `forceEmailQuery` is active (`PlatformInfo.isWeb && AppConfig.isForceEmailQueryEnabled`). Do not flag the absence of a session capability gate as a bug for this getter.

Learnt from: dab246
Repo: linagora/tmail-flutter PR: 4380
File: docs/adr/0072-thread-aware-bulk-actions-email-list-collapse-threads.md:265-314
Timestamp: 2026-03-17T10:08:56.785Z
Learning: In linagora/tmail-flutter, the thread-aware interactors introduced in ADR-0072 (e.g., MoveThreadInteractor, MarkAsThreadReadInteractor in docs/adr/0072-thread-aware-bulk-actions-email-list-collapse-threads.md) are intentionally designed to call EmailRepository directly, rather than delegating to existing email interactors (e.g., MoveMultipleEmailToMailboxInteractor, MarkAsMultipleEmailReadInteractor). This is a deliberate choice to avoid affecting old logic. Do not suggest refactoring them to reuse existing interactors.

Learnt from: dab246
Repo: linagora/tmail-flutter PR: 4194
File: lib/features/manage_account/presentation/manage_account_dashboard_controller.dart:174-176
Timestamp: 2025-12-09T09:36:45.349Z
Learning: In Dart/Flutter projects using GetX, do not wrap getBinding<T>() calls in try/catch since they return null when not found. Only wrap Get.find<T>() calls in try/catch because they throw if a dependency is unavailable. When a binding is optional, consider checking Get.isRegistered<T>() or handling the null/exception path gracefully instead of blindly catching, and document the expectation for failure modes where a dependency may not be registered.

Learnt from: dab246
Repo: linagora/tmail-flutter PR: 4315
File: core/lib/utils/app_logger.dart:140-144
Timestamp: 2026-02-09T07:57:07.651Z
Learning: For the tmail-flutter repository, ensure that logTrace calls are used selectively for diagnostic purposes related to Sentry submissions and not for regular production logging. During reviews, verify that trace-level logs are placed only where they meaningfully aid diagnosis, avoid noisy production reports, and ensure no sensitive data is logged.

Copy link
Copy Markdown

@codescene-delta-analysis codescene-delta-analysis bot left a comment

Choose a reason for hiding this comment

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

Code Health Improved (1 files improve in Code Health)

Gates Passed
3 Quality Gates Passed

See analysis details in CodeScene

View Improvements
File Code Health Impact Categories Improved
mailbox_isolate_worker.dart 9.10 → 9.39 Complex Conditional, Excess Number of Function Arguments

Absence of Expected Change Pattern

  • tmail-flutter/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart is usually changed with: tmail-flutter/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart, tmail-flutter/lib/features/mailbox/data/datasource/mailbox_datasource.dart, tmail-flutter/lib/features/mailbox/data/repository/mailbox_repository_impl.dart

Quality Gate Profile: The Bare Minimum
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

@tddang-linagora
Copy link
Copy Markdown
Collaborator Author

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants