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

Improve data fetching in Order Details, to avoid I/O performance on the main thread #14999

Merged
merged 20 commits into from
Mar 28, 2025

Conversation

pmusolino
Copy link
Member

Closes: #14923

Description

We observed that while opening product details, it's easy to see the following warning, generated by multiple reads on storage (Core Data). Also, this issue sometimes generate a lag opening the Order Detail screen.

Image

This warning is occurring because we're making a lot of different fetch requests when loading the details, causing the hang issue.

I solved the issue by removing two fetches from OrderDetailsResultsControllers:

  • updating the feeLines property to return the relationship from order.fees instead, of using a Results Controller to fetch the order fees.
  • I added the shippingLabels property to Order which was connected in Storage with a relationship, but not present inside the Networking model.
    • Update Networking.generated.swift, Models+Copiable.generated.swift, and Order.swift to include shippingLabels property in Order struct.
    • Update OrderDetailsResultsControllers.swift by removing shippingLabelResultsController and using order.shippingLabels instead.
    • Update various files to incorporate shippingLabels property in Order creation and manipulation.
    • Add Sendable conformance to ShippingLabel, ShippingLabelAddress, ShippingLabelRefund, ShippingLabelRefundStatus, and ShippingLabelStatus structs and enums for concurrency safety.
    • Update tests in OrderDetailsDataSourceTests.swift to handle shippingLabels within Order instances.

Those were the only relationships with Order. In my opinion, we could potentially improve performance further by handling some fetches externally (e.g., fetching all order statuses). However, the current implementation is structured such that each view/model doesn't have visibility beyond the order itself. While this approach is possible, we need to discuss how deep we want to go with such changes. The current solution, however, does address the I/O problem effectively.

Steps to reproduce

Open multiple times different Order Details. You can notice a purple warning mentioning Performing 1/0 on the main thread by reading or writing to a database can cause slow launches. in ResultsController.

Testing information

You should not be able anymore to replicate the warning after these changes.

  • Make sure you are able to see “Custom amounts” section adding a fee, and that editing the fee update the fee title/amount.
  • Test that Shipping Labels in Order Details works like before if the plugin is installed: fetching/creation/update.
    - Ensure the "Create Shipping Label" button is visible when the order is eligible for shipping label creation and has no existing labels.
    - Ensure the "Create Shipping Label" button is not visible when the order is eligible but already has shipping labels.
    - Ensure the "More" button is visible in the product section when the order is eligible and has no refunded shipping labels.
    - Ensure the "More" button is visible in the product section when the order is eligible and has refunded shipping labels.
    - Ensure the "More" button is not visible in the product section when the order is not eligible for shipping label creation.
    - Ensure that refunded shipping labels are correctly identified and handled, and that the UI reflects this state appropriately.
    - Confirm that the order is correctly marked as eligible for shipping label creation based on predefined criteria (e.g., order status, payment status).
    - Ensure that any changes to the shipping labels (e.g., addition, refund) are immediately reflected in the UI without requiring a manual refresh.

  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

Reviewer (or Author, in the case of optional code reviews):

Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement:

  • The PR is small and has a clear, single focus, or a valid explanation is provided in the description. If needed, please request to split it into smaller PRs.
  • Ensure Adequate Unit Test Coverage: The changes are reasonably covered by unit tests or an explanation is provided in the PR description.
  • Manual Testing: The author listed all the tests they ran, including smoke tests when needed (e.g., for refactorings). The reviewer confirmed that the PR works as expected on all devices (phone/tablet) and no regressions are added.

…oved `feeLinesResultsController` and related logic
…ge, but not present inside the Networking model. Doing this to reduce number of `ResultsController` from `OrderDetailsResultsControllers`:

- Update `Networking.generated.swift`, `Models+Copiable.generated.swift`, and `Order.swift` to include `shippingLabels` property in `Order` struct
- Update `OrderDetailsResultsControllers.swift` by removing `shippingLabelResultsController` and using `order.shippingLabels` instead
- Update various files to incorporate `shippingLabels` property in Order creation and manipulation
- Add `Sendable` conformance to `ShippingLabel`, `ShippingLabelAddress`, `ShippingLabelRefund`, `ShippingLabelRefundStatus`, and `ShippingLabelStatus` structs and enums for concurrency safety
- Update tests in `OrderDetailsDataSourceTests.swift` to handle `shippingLabels` within `Order` instances
@pmusolino pmusolino added the feature: order details Related to order details. label Jan 28, 2025
@pmusolino pmusolino added this to the 21.6 milestone Jan 28, 2025
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Jan 28, 2025

WooCommerce iOS📲 You can test the changes from this Pull Request in WooCommerce iOS by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS WooCommerce iOS
Build Numberpr14999-bbfb18c
Version22.0
Bundle IDcom.automattic.alpha.woocommerce
Commitbbfb18c
App Center BuildWooCommerce - Prototype Builds #13442
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@selanthiraiyan
Copy link
Contributor

Thanks for working on this, @pmusolino!

Not directly related to this PR: While attempting to test this PR I noticed that the "Add Extension To Store" from the following screen doesn't work. I also noticed that the code is not linked to any action. Code link

Video recording:

Simulator.Screen.Recording.-.iPhone.15.Pro.Max.-.2025-01-29.at.11.13.48.mp4

@selanthiraiyan selanthiraiyan self-assigned this Jan 29, 2025
@selanthiraiyan
Copy link
Contributor

selanthiraiyan commented Jan 29, 2025

I noticed one more issue that is probably not related to this PR.

I have installed WooCommerce Shipping on my JN store, but I still see the "Get WooCommerce Shipping" banner in the Order Details screen.

Simulator.Screen.Recording.-.iPhone.15.Pro.Max.-.2025-01-29.at.11.22.44.mp4

I have tried restarting the app (stop and run again from Xcode), but the banner doesn't go away.

Copy link
Contributor

@selanthiraiyan selanthiraiyan left a comment

Choose a reason for hiding this comment

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

The following issue happens in trunk as well and I don't think this is related to this PR. IMO, this looks related to this CUW post - peaMlT-Yo-p2

After creating a shipping label on my JN store I started facing issues with loading orders.

I tried emptying the orders creating Woo Smooth Generator from my JN store and kept only the order that I manually created from the mobile app. But still the mobile app fails to load that order and shows the following decoding error in logs.

⛔️ Error synchronizing orders: typeMismatch(Swift.Int64, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), _JSONKey(stringValue: "Index 4", intValue: 4), CodingKeys(stringValue: "line_items", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "taxes", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "id", intValue: nil)], debugDescription: "Expected to decode Int64 but found a string instead.", underlyingError: nil))

My JN site has the following plugins installed.
Screenshot 2025-01-29 at 2 26 09 PM

Please let me know if you need an invitation to my JN site.

@selanthiraiyan selanthiraiyan removed their assignment Jan 29, 2025
@rachelmcr
Copy link
Contributor

Not directly related to this PR: While attempting to test this PR I noticed that the "Add Extension To Store" from the following screen doesn't work. I also noticed that the code is not linked to any action.

FWIW the "Get WooCommerce Shipping" banner with this install prompt is behind the shippingLabelsOnboardingM1 feature flag. I believe that was an older Kiwi project that never shipped; to avoid confusion it's probably worth removing the code or entirely disabling that feature flag.

@selanthiraiyan
Copy link
Contributor

@pmusolino While trying to Refund a shipping label, I found that the "Request refund" button doesn't do anything.

Simulator Screen Recording - iPhone 15 Pro Max - 2025-01-30 at 12 11 03

Copy link
Contributor

@selanthiraiyan selanthiraiyan left a comment

Choose a reason for hiding this comment

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

@pmusolino 👋

Quoting the following from the "Testing Information" in the PR description

  • Ensure that any changes to the shipping labels (e.g., addition, refund) are immediately reflected in the UI without requiring a manual refresh.

I noticed that the order detail page doesn't reflect the newly purchased shipping label unless I pull down to refresh. Even after I pull to refresh, I see the "Create Shipping Label" button, and it goes away after another pull down to refresh.

Simulator.Screen.Recording.-.iPhone.15.Pro.Max.-.2025-01-30.at.12.13.22.mp4

@pmusolino
Copy link
Member Author

I noticed one more issue that is probably not related to this PR.
I have installed WooCommerce Shipping on my JN store, but I still see the "Get WooCommerce Shipping" banner in the Order Details screen.

I filled an issue here about removing the feature flag.

@pmusolino
Copy link
Member Author

@pmusolino While trying to Refund a shipping label, I found that the "Request refund" button doesn't do anything.

This doesn't work in trunk either because I think it's still a WIP under a feature flag. cc @rachelmcr

@pmusolino
Copy link
Member Author

I noticed that the order detail page doesn't reflect the newly purchased shipping label unless I pull down to refresh. Even after I pull to refresh, I see the "Create Shipping Label" button, and it goes away after another pull down to refresh.

I tested it by creating the shipping label from the web, and it works; it's visible in the app when opening the order. I also tried creating the shipping label from the app. In this specific case, the refresh does not work as you mentioned, but it also doesn't refresh in the trunk branch. I've asked in C02KUCFCSFP/p1738239651479579 if this is part of a new flow, since I remember the UI for creating the shipping label was different a while back. In any case, it's not something related to this PR. 👍

@@ -239,6 +245,9 @@ public struct Order: Decodable, Sendable, GeneratedCopiable, GeneratedFakeable {
return OrderAttributionInfo(metaData: allOrderMetaData)
}()

// Shipping labels
let shippingLabels = try container.decodeIfPresent([ShippingLabel].self, forKey: .shippingLabels) ?? []
Copy link
Contributor

Choose a reason for hiding this comment

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

👋 @pmusolino Do we receive this shipping_labels key in orders response? Based on my testing in a Woo store with shipping labels, I don't see it being sent in wc/v3/orders.

If we do receive the shipping_labels, should we update the mock JSON and add unit tests to ensure that shipping_labels are mapped correctly?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question, @selanthiraiyan. In ShippingLabelStore, the method upsertShippingLabels connects the fetched shipping labels to the specific order, so the order doesn't return specific shipping labels, making it unnecessary to decode the Shipping Labels in the Order model. However, declaring the property (without decoding) in the networking model seems necessary to access shippingLabels, unless you have another solution in mind.

Yes, we could access the shipping labels from storage, but that's what we're trying to avoid. Do you think I can leave the shippingLabels property in Order.swift but remove the decoding? This way, by default, it would be an empty array.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, @pmusolino! I don't have a solution in mind. But I think we should remove the decoding, as it will be misleading.

I'm tagging @itsmeichigo to check whether she has any other solutions.

Copy link
Contributor

@itsmeichigo itsmeichigo Feb 4, 2025

Choose a reason for hiding this comment

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

Thanks for the ping Sharma!

I think if the key for shipping labels is non-existent in the order response, keeping the property an empty array should be the way to go. There's no need to waste more resources to look for such a key to decode. Please also leave a comment in the code to explain why the value is empty by default, and when it's going to be updated.

However, I'm concerned about this part:

In ShippingLabelStore, the method upsertShippingLabels connects the fetched shipping labels to the specific order

In the order details screen, after syncing shipping labels, we'd need to ensure that the order in the view model is updated with the fetched label(s), just so they can be displayed on the UI.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you both! I updated the decoding here 85c0115 the comment should be clear enough.

In the order details screen, after syncing shipping labels, we'd need to ensure that the order in the view model is updated with the fetched label(s), just so they can be displayed on the UI.

Yes, this already happens from my testing. If you create or update a shipping label, or open an order detail with an existing shipping label that needs to be fetched remotely, you will see the order detail UI correctly updated.

@wpmobilebot wpmobilebot modified the milestones: 21.6, 21.7 Jan 31, 2025
@wpmobilebot
Copy link
Collaborator

Version 21.6 has now entered code-freeze, so the milestone of this PR has been updated to 21.7.

@pmusolino
Copy link
Member Author

@selanthiraiyan when I replied in that discussion with Huong, I wasn't aware of the problem mentioned here by Rachel.

Why?
In my testing, I was always pressing "Update" for updating the order after creating a shipping label, while I discovered this is not needed for generating and attaching a shipping label on the web (In fact, I believe it is a web issue because the Update button is enabled even if there are no changes).
Pressing "Update", changes the behavior of the refresh of the shipping labels, and the reason is explained in this Rachel's comment:

When we sync the order, if the order already exists in storage, we use the last modified date to check if it has been updated remotely. (This date doesn't change if shipping labels are added to the order, since those are stored separately in the database and not considered part of the order

So, when you create a shipping label from the web without saving the order, the order's modification date does not update. As a result, everything does not refresh correctly because the shipping label and the order are treated as two separate entities. However, if you refresh the order, the shipping labels will be attached correctly.

When we get the shipping labels from the syncShippingLabels() method, now we need to explicitly attach them to the order using:

if let updatedOrder = self?.order.copy(shippingLabels: shippingLabels) {
    self?.update(order: updatedOrder)
}

Previously, this was unnecessary because we were retrieving data from storage.
Without this, the UI would continue to use the existing order object in memory, which is not updated.

Let me know if I have cleared your doubts, Sharma. 🙌

@wpmobilebot wpmobilebot modified the milestones: 21.9, 22.0 Mar 7, 2025
@wpmobilebot
Copy link
Collaborator

Version 21.9 has now entered code-freeze, so the milestone of this PR has been updated to 22.0.

@selanthiraiyan
Copy link
Contributor

Thanks for the detailed answer, @pmusolino!

Would it work to sync the shipping labels first before loading the order from the storage?

  1. Send the remote request to fetch shipping labels.
  2. Update the order in storage with the shipping label.
  3. Send a request to retrieve the order.
  4. If the order has not been modified recently, we will still end up loading the latest order with shipping labels from storage.

In the above approach,

  • We also avoid manually updating the Order in the view layer.
  • I assume we can also avoid showing the "Create Shipping Labels" momentarily.

@wpmobilebot wpmobilebot modified the milestones: 22.0, 22.1 Mar 21, 2025
@wpmobilebot
Copy link
Collaborator

Version 22.0 has now entered code-freeze, so the milestone of this PR has been updated to 22.1.

@pmusolino
Copy link
Member Author

@selanthiraiyan Not sure this is the ideal solution because the core of Order Detail is the sync of the order in OrderDetailsViewModel. From there, there is a list of nested operations, including shipping labels. So, the initial info of the order is displayed, and other sections are loaded later. If we first load Shipping Labels, we create a series of dependencies in reverse because we should first check the plugins to see if the shipping labels are supported, then fetch the shipping labels if they are supported, and then fetch the order to retrieve it for the first time or update it. This would also complicate the logic, seemingly with no advantages, and, in fact, it might worsen the experience at the UI level (eg. now there is a bar displaying that the order is still loading, but the main info is already displayed).

@selanthiraiyan
Copy link
Contributor

Thanks for the explanation, @pmusolino! I understand. Manually updating the order with the synched shipping labels seems like the way to go. Do you plan to add those changes to this PR?

@pmusolino
Copy link
Member Author

@selanthiraiyan if you think the work I did here feat/batch-fetching-in-order-details...feat/batch-fetching-in-order-details-take-2 works, I can merge the changes in this PR 🙌

@selanthiraiyan
Copy link
Contributor

I can merge the changes in this PR 🙌

@pmusolino Yes, please go ahead with this. Thanks!

@pmusolino
Copy link
Member Author

@selanthiraiyan done ✅ waiting for the final approval

Copy link
Contributor

@selanthiraiyan selanthiraiyan left a comment

Choose a reason for hiding this comment

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

Thanks for your patience with this work, Paolo! ❤️

Code

I left a few minor comments and one question about the code. We are getting pretty close to merging this PR. 👏

Testing

  1. I validated that a label purchased from web is shown in the order details page. This test case scenario.
  2. The order details page doesn't reflect a newly purchased shipping label unless I pull down to refresh. Mentioned in this previous comment I tested that this bug exists in trunk as well. ✅

👋 @itsmeichigo It would be great if you could you take a look at this PR as a confidence check? Thanks! 🙇

group.leave()
}
// Check Woo Shipping support first, to ensure correct flows are enabled for shipping labels.
self?.dataSource.isEligibleForWooShipping = ((await self?.isWooShippingSupported()) != nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

We could unwrap self using a guard statement to avoid using self as an optional here and a few other places below.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done in bbfb18c.

self?.dataSource.isEligibleForShippingLabelCreation = isEligible ?? false

// Sync shipping labels and update order with the result if available
if let shippingLabels = await self?.syncShippingLabels() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we call syncShippingLabels even if checkShippingLabelCreationEligibility returned false above? If yes, can we start this request asynchronously without waiting for the response from checkShippingLabelCreationEligibility?

Copy link
Contributor

Choose a reason for hiding this comment

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

We still need to sync shipping labels just in case the creation eligibility is only updated recently. I moved these calls to a task group to ensure they don't block each other in bbfb18c.

@itsmeichigo
Copy link
Contributor

Thanks for the ping @selanthiraiyan! I don't have any additional comment aside from your latest ones.

Regarding this point:

The order details page doesn't reflect a newly purchased shipping label unless I pull down to refresh. #14999 (review) I tested that this bug exists in trunk as well.

I did some debugging, and this seems to be an issue with viewWillAppear not being triggered after dismissing the shipping label creation flow. This was caused by the shipping label creation flow being presented with overFullScreen mode.

I fixed this by switching to fullScreen mode for both the shipping label creation flow and order edit flow in 32dbb12. This ensures that the order is synced again after dismissing these flows. The created shipping labels should now be displayed after dismissing the creation flow.

@itsmeichigo
Copy link
Contributor

@selanthiraiyan I added the final changes following your latest comments, please take another look when you can.

Copy link
Contributor

@selanthiraiyan selanthiraiyan left a comment

Choose a reason for hiding this comment

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

Thanks for handling this, Huong! The changes LGTM.

Let's merge this PR. 🚀

@itsmeichigo itsmeichigo merged commit 02c2794 into trunk Mar 28, 2025
13 checks passed
@itsmeichigo itsmeichigo deleted the feat/batch-fetching-in-order-details branch March 28, 2025 07:14
@pmusolino
Copy link
Member Author

Thank you, Huong, for taking care of the remaining changes! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature: order details Related to order details.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

UI freeze for about 1 second when opening an Order Detail
5 participants