Skip to content

Fix iOS receipt multi-select OOM: skip decoding hidden previews + sequence conversions#94976

Open
dilshodmackbook-sketch wants to merge 4 commits into
Expensify:mainfrom
dilshodmackbook-sketch:dilshod/fix-93846-sequence-multiselect
Open

Fix iOS receipt multi-select OOM: skip decoding hidden previews + sequence conversions#94976
dilshodmackbook-sketch wants to merge 4 commits into
Expensify:mainfrom
dilshodmackbook-sketch:dilshod/fix-93846-sequence-multiselect

Conversation

@dilshodmackbook-sketch

@dilshodmackbook-sketch dilshodmackbook-sketch commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

This is the fix for the iOS receipt multi-select OOM (std::bad_alloc, Sentry APP-63S). Profiling the documented repro (30 HEIC receipts through the Scan flow) shows the peak comes from two independent memory spikes, so this PR addresses both:

1. Hidden receipt previews decode every draft receipt (dominant spike).
ReceiptPreviews is mounted whenever multi-scan is available (canUseMultiScan && onMultiScanSubmit), not gated on isMultiScanEnabled. When multi-scan is off the strip only animates its height/width to 0, but the FlatList still mounts one item per draft receipt and renders a full <Image>, decoding every draft receipt into native memory while nothing is visible. On a large multi-select (e.g. 30 HEIC) all of them decode offscreen at once.

Fix: render the placeholder (not the <Image>) until multi-scan is enabled, so hidden receipts are never decoded. isMultiScanEnabled is passed as extraData so the cells re-render when the flag flips.

2. Concurrent HEIC→JPEG conversions at pick time.
When several images are multi-selected, AttachmentPicker fires every HEIC→JPEG conversion concurrently — the loop kicks off each verifyFileFormat(...).then(renderAsync()) without awaiting the previous one, so N large photos decode into native memory at the same time.

Fix: on iOS, process picked assets one at a time so at most one heavy decode is alive at any moment. Other platforms keep the original concurrent behavior. The strategy is resolved by file extension (see processAssets/index.ios.ts).

Together these keep peak native memory bounded through the multi-image scan flow, where previously a large batch could OOM on iOS.

Memory profiling (iOS, documented 30-HEIC Scan repro, Xcode Memory report):

Build Peak (High) Shape
Before (main) 6.71 GB tall, sustained spike during processing
After (this PR) 3.42 GB flat, no spike

Peak memory drops 6.71 GB → 3.42 GB (~49%). Sequencing removes the bulk (the concurrent 30-decode spike); the ReceiptPreviews placeholder trims the remaining offscreen decodes. Numbers are from a standalone dev build on the simulator, so absolute values are inflated, but the relative delta holds. Before/after videos are in the iOS: Native section below.

Fixed Issues

$ #93846
PROPOSAL: #93846 (comment)

Tests

Precondition: download the HEIC image from #68778 (comment), duplicate it into 30 copies, and import them into iOS Photos.

  1. On an iOS device/simulator, from a fresh launch, attach Xcode's Memory report next to the running app.
  2. Tap the floating Scan button.
  3. Tap the image picker icon → Choose file.
  4. Select the 30 HEIC images and confirm.
  5. Finish the expense creation flow.
  6. Verify the app does not crash or get killed during processing, and native memory stays bounded (no full-resolution spike from hidden previews or concurrent decodes).
  7. Enable multi-scan and verify the receipt previews still render correctly (images appear, not just placeholders).
  8. Attach a single image and verify it still converts/uploads correctly with the right orientation and quality.
  9. Repeat a multi-select on Android and verify image attachment still works as before (concurrent, unchanged behavior).
  10. Verify that no errors appear in the JS console.
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline.
  2. Multi-select and attach several images (including HEIC on iOS) to a chat / scan flow.
  3. Verify the images appear as pending and are queued.
  4. Go back online and verify the queued images upload successfully.

QA Steps

Precondition: import ~30 large HEIC photos into iOS Photos.

  1. On iOS, from a fresh launch, tap the floating Scan button → image picker → Choose file.
  2. Select 30 large photos and finish the expense creation flow.
  3. Verify all images upload correctly with the right orientation/quality and the app does not crash during processing.
  4. Enable multi-scan and verify receipt previews render correctly.
  5. On Android, multi-select images and verify behavior is unchanged.
  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
video_2026-07-09_18-43-59.mp4
Android: mWeb Chrome
Screen.Recording.2026-07-09.at.16.19.55.mov
iOS: Native

30-HEIC Scan repro, Xcode Memory report next to the app (standalone dev build on the simulator).

Before (main) — peak 6.71 GB:

before-6.71GB.mp4

After (this PR) — peak 3.42 GB:

after-3.42GB.mp4
iOS: mWeb Safari
Screen.Recording.2026-07-09.at.16.22.19.mov
MacOS: Chrome / Safari
Screen.Recording.2026-07-09.at.15.50.57.mp4
Screen.Recording.2026-07-09.at.16.11.37.mp4

When several images are picked at once, the AttachmentPicker fires every HEIC
conversion concurrently, so N full-resolution bitmaps can decode simultaneously.
On iOS HybridApp, where the process shares a single jetsam budget with OldDot,
that concurrent spike contributes to the std::bad_alloc OOM (Sentry APP-63S).

Process picked assets one at a time on iOS so at most one heavy decode is alive;
other platforms keep the original concurrent behavior. This is the sequencing
half of the Expensify#93846 fix, split out from the decode cap so its memory impact can
be validated and adopted independently.
@dilshodmackbook-sketch dilshodmackbook-sketch requested review from a team as code owners June 30, 2026 13:26
@melvin-bot melvin-bot Bot requested review from a team, QichenZhu and flaviadefaria and removed request for a team June 30, 2026 13:26
@melvin-bot

melvin-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

@QichenZhu Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot Bot requested review from mountiny and removed request for a team June 30, 2026 13:26
@melvin-bot

melvin-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

@mountiny Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

// process shares a single jetsam budget with OldDot, so converting the whole selection at once can
// allocate N large bitmaps simultaneously and OOM. There we process assets one at a time to keep at
// most one heavy decode alive; other platforms keep the original concurrent behavior (per #93846).
const processAssets = async () => {

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.

❌ CONSISTENCY-1 (docs)

This change adds a runtime Platform.OS === 'ios' branch inside the component to give iOS sequential processing and other platforms concurrent processing. Mixing platform-specific control flow inside a shared component is exactly the pattern CONSISTENCY-1 discourages — it increases branching complexity and platform-specific bug risk, and the .native.tsx/.web.tsx exception does not cover an iOS-vs-Android split done via a runtime check. There is currently no index.ios.tsx/index.android.tsx in this folder, so the divergence is being handled with an in-component check rather than file extensions.

Prefer separating the platform-specific behavior through file extensions. For example, extract the asset-processing strategy into a platform-resolved helper:

// processAssets.ios.ts — sequential to keep at most one heavy HEIC decode alive
export default async function processAssets(assets: Asset[], processAsset: (a: Asset) => Promise<void>) {
    for (const asset of assets) {
        // eslint-disable-next-line no-await-in-loop -- decode one at a time to avoid OOM on iOS HybridApp
        await processAsset(asset);
    }
}

// processAssets.ts (default/android) — concurrent
export default async function processAssets(assets: Asset[], processAsset: (a: Asset) => Promise<void>) {
    await Promise.all(assets.map(processAsset));
}

Then call the resolved processAssets(assets, processAsset) in index.native.tsx without the Platform.OS check, letting the bundler pick the correct implementation per platform.


Reviewed at: b65b34b | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call. Refactored to extension-based resolution: the concurrent/sequential strategy now lives in a platform-resolved processAssets helper (concurrent.ts default, sequential.ts on iOS via index.ios.ts), so the component just calls processAssets(assets, processAsset) with no Platform.OS branch. Added unit tests for both strategies.

@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
...nents/AttachmentPicker/processAssets/concurrent.ts 100.00% <100.00%> (ø)
...nents/AttachmentPicker/processAssets/sequential.ts 100.00% <100.00%> (ø)
...questStepScan/components/ReceiptPreviews/index.tsx 94.44% <100.00%> (ø)
src/components/AttachmentPicker/index.native.tsx 20.53% <0.00%> (+1.20%) ⬆️
... and 14 files with indirect coverage changes

…a runtime check

Addresses the CONSISTENCY-1 review note: rather than branching on Platform.OS === 'ios'
inside the component, extract the concurrent/sequential strategy into a platform-resolved
processAssets helper (concurrent.ts default, sequential.ts on iOS via index.ios.ts). The
component just calls processAssets(assets, processAsset) and lets the bundler pick the
implementation. Adds unit tests covering both strategies.
@dilshodmackbook-sketch dilshodmackbook-sketch force-pushed the dilshod/fix-93846-sequence-multiselect branch from fe5da76 to 1631cdd Compare June 30, 2026 14:30
@mountiny

Copy link
Copy Markdown
Contributor

@codex review
@MelvinBot review

@github-actions

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

dilshodmackbook-sketch and others added 2 commits July 6, 2026 15:02
…equence-multiselect

# Conflicts:
#	src/components/AttachmentPicker/index.native.tsx
ReceiptPreviews is mounted whenever multi-scan is available (canUseMultiScan &&
onMultiScanSubmit), not gated on isMultiScanEnabled. When multi-scan is off the
strip only animates its height/width to 0, but the FlatList still mounts an item
per draft receipt and renders a full <Image>, decoding every draft receipt into
native memory while nothing is visible. On iOS a large multi-select (e.g. 30 HEIC
receipts) decodes them all offscreen and can spike native memory into an OOM (Expensify#93846).

Render only the placeholder until multi-scan is enabled so hidden receipts are
never decoded, and pass isMultiScanEnabled as extraData so cells re-render when
the flag flips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dilshodmackbook-sketch dilshodmackbook-sketch changed the title Sequence iOS multi-select image conversions to reduce peak memory Fix iOS receipt multi-select OOM: skip decoding hidden previews + sequence conversions Jul 6, 2026
@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Folded the ReceiptPreviews fix into this PR so #94976 now covers both spikes from #93846: hidden previews no longer decode when multi-scan is off, and the iOS conversions are sequenced. Updated the description and test steps to the Scan-flow repro (30 HEIC). Planning to close #94969 in favor of this.

@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Profiled the documented 30-HEIC Scan repro with Xcode's Memory report (before/after videos in the PR body). Peak drops 6.71 GB -> 3.42 GB (~49%). The sequencing removes the bulk (the concurrent 30-decode spike), and the ReceiptPreviews placeholder trims the remaining offscreen decodes. This is a standalone dev build on the simulator so the absolute numbers are inflated, but the relative delta holds and it lines up with the two spike sources we discussed.

@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

@QichenZhu friendly bump on this one whenever you get a chance. Everything's ready on my end: both spikes from #93846 are addressed, the 30-HEIC Scan repro is profiled (peak 6.71 GB -> 3.42 GB), and CI is green. Happy to walk through anything if it helps the review.

@QichenZhu

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
iOS: mWeb Safari
MacOS: Chrome / Safari

@QichenZhu

QichenZhu commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

@dilshodmackbook-sketch good progress! This PR works for low traffic accounts, but for high traffic accounts, the app still crashes after you tap Create expenses.

after.mov

Could you dig further?

A likely culprit is retryParams. I'm not sure it's dead code, but it consumes a lot of memory. I suggest we solve it in a follow-up PR so it can be reverted separately just in case.

@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Thanks @QichenZhu, great lead — I dug into retryParams and I think you're right that it's the main offender here.

For each receipt, requestMoney builds retryParams from nearly the whole request payload, and getReceiptError then JSON.stringifys it into that transaction's errors (failureData). The heavy part is policyParams — it carries policyCategories and policyTagList, which are large on high traffic workspaces. So we serialize that big object once per receipt and write it into Onyx, and with a 30-receipt batch on a big workspace that's what pushes it over the edge. Low traffic accounts have small policies, so the strings stay small and it doesn't crash. There's already a precedent for trimming this — retryParams for track expense strips reportActionsList "to keep the serialized error JSON small", so extending that to drop the heavy policy collections (and the full report) fits the existing pattern.

Agreed on keeping it a separate follow-up PR so it can be reverted independently. I'll confirm what retryParams actually needs on retry, profile the before/after, and open it. This PR is an independent, measured win on the decode side, so I'd like to keep it moving as-is in the meantime.

@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Quick scoping question before I open the follow-up: the high traffic crash turned out to be a separate root cause from the decode spike this PR fixes — it's the retryParams serialization in a different code path, not the image decode. Would you prefer I track the follow-up as its own issue/job, or keep it under #93846? Just want to scope it correctly before I start — happy either way.

@QichenZhu

Copy link
Copy Markdown
Contributor

@dilshodmackbook-sketch, it is the same issue, just with multiple causes.

@QichenZhu

Copy link
Copy Markdown
Contributor

@mountiny, could you trigger an ad-hoc build again?

Also, what do you think of this plan?

This PR works for low traffic accounts, but for high traffic accounts, the app still crashes after you tap Create expenses.

A likely culprit is retryParams. I'm not sure it's dead code, but it consumes a lot of memory. I suggest we solve it in a follow-up PR so it can be reverted separately just in case.

@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Sounds good, I'll keep it under #93846 and move the retryParams fix into a separate follow-up PR so it can be reverted on its own.

One thing I confirmed while digging: retryParams gets stringified into every failed transaction's error, but nothing on the NewDot side actually reads it back — the receipt error row only offers Save receipt / Delete, there's no retry path that consumes it. The heavy part is policyParams (policyCategories + policyTagList), which scales with workspace size, so I'll strip those. Follow-up incoming.

@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Follow-up is up: #95663. It strips the large policy collections (policyCategories / policyTagList) from the requestMoney retryParams so they're no longer stringified into every failed transaction's error. Kept separate from this PR so it can be reverted independently.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@QichenZhu

QichenZhu commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Update: I retested today and saw no major difference before and after this fix, even though it used to work. Let me check if recent changes on main affected things.

iPhone 17 - Low-traffic account - Before fix - No crash

before-iphone-17-low.mov

iPhone 17 - Low-traffic account - After fix - No crash

after-iphone-17-low.mov

iPad 9 - Low-traffic account - Before fix - Crash

before-ipad-9-low.mov

iPad 9 - Low-traffic account - After fix - Crash

after-ipad-9-low.mov

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.

3 participants