Skip to content

[eas-cli] Pre-warm bsdiff patches against top-K base and embedded updates#3869

Open
jc-expo wants to merge 5 commits into
mainfrom
jc-expo/eng-21079-metrics-based-prewarmer
Open

[eas-cli] Pre-warm bsdiff patches against top-K base and embedded updates#3869
jc-expo wants to merge 5 commits into
mainfrom
jc-expo/eng-21079-metrics-based-prewarmer

Conversation

@jc-expo

@jc-expo jc-expo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Why

Linear ENG-21079.

The previous prewarmDiffingAsync base-update selection was too simple: it always picked updateIds[1] — the second-most-recent update on the branch (filtered by platform + runtimeVersion). If the second-newest was a quick hotfix on top of the newest, most devices in the field are likely on the third newest (or older), so the pre-warmed bsdiff patch misses the base that real traffic actually requests.

When a device requests a base that hasn't been pre-warmed, the server kicks off the diff on-demand but, until that patch is ready, serves the full un-diffed bundle. So a bad base guess means those clients download the entire update instead of a small patch until the diff finishes computing.

How

Pre-warm against the top-K candidates instead of a single base, approximating the bases that the lazy path organically catches from real device traffic:

  • Issue HEAD requests against the top 5 most recent updates on the branch (skipping the current one) rather than just the second-most-recent.
  • Also pre-warm patches from the registered embedded bundles (up to 2) to the new update — the fresh-install scenario — falling back to the sentinel empty-embedded id when a project has no registered embedded bundle.
  • Extracted the per-update logic into prewarmUpdateDiffsAsync, kept it best-effort (failures are logged via Log.debug and swallowed, never blocking a publish), and added debug logging throughout.

Test Plan

  • yarn test for the eas-cli package passes.
  • Manually published an update with EXPO_DEBUG=1 and confirmed the debug logs show the top-K recent updates and embedded bundles being queued and HEAD-requested with the a-im: bsdiff diffing headers.

…ates

Instead of only diffing against the second-most-recent update on the
branch, issue HEAD requests for the top-K most recent updates and the
registered embedded bundles, approximating the lazy path's organic base
selection from real device traffic.
@linear-code

linear-code Bot commented Jun 16, 2026

Copy link
Copy Markdown

ENG-21079

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.48485% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 59.02%. Comparing base (3e39365) to head (dd189b3).
⚠️ Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
packages/eas-cli/src/update/utils.ts 98.49% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3869      +/-   ##
==========================================
+ Coverage   58.45%   59.02%   +0.58%     
==========================================
  Files         922      932      +10     
  Lines       40199    40794     +595     
  Branches     8462     8597     +135     
==========================================
+ Hits        23493    24076     +583     
- Misses      16610    16622      +12     
  Partials       96       96              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jc-expo jc-expo requested a review from quinlanj June 16, 2026 23:39
jc-expo added 2 commits June 17, 2026 08:56
Have prewarmDiffingAsync return the list of successfully pre-warmed
bsdiff patches so the top-K base + embedded-bundle selection can be
asserted on, add tests for it, and route the prewarm HEAD through the
../fetch wrapper (proxy support + standard mocking).
Add minimal tests for the no-recent-updates, no-signed-url, and
no-launch-asset cases to reach full patch coverage.
@jc-expo jc-expo marked this pull request as ready for review June 17, 2026 18:43
@github-actions

Copy link
Copy Markdown

Subscribed to pull request

File Patterns Mentions
**/* @douglowder
packages/eas-cli/src/update/** @wschurman, @quinlanj

Generated by CodeMention

@quinlanj quinlanj left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

looking good so far! 💯

Comment thread packages/eas-cli/src/update/utils.ts Outdated
// registered embedded bundle.
const embeddedUpdateQuery = await EmbeddedUpdateQuery.viewPaginatedAsync(graphqlClient, {
appId,
filter: { platform, runtimeVersion: update.runtimeVersion },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you'll need to also filter by channel to get the set of builds (with embedded updates) that are eligible to receive the published update

@github-actions

Copy link
Copy Markdown

✅ Thank you for adding the changelog entry!

@jc-expo jc-expo requested a review from quinlanj June 18, 2026 21:54
Comment on lines +315 to +326
const channels = await ChannelQuery.viewUpdateChannelsBasicInfoPaginatedOnAppAsync(
graphqlClient,
{
appId,
first: PREWARM_CHANNELS_LIMIT,
}
);
return channels.edges
.map(edge => edge.node)
.filter(channel => getBranchIds(getBranchMapping(channel.branchMapping)).includes(branchId))
.map(channel => channel.name);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I forgot there's no graphql api for this yet. Can you open up a separate pr and add a channelPaginated edge off the UpdateBranch node to implement relay pagination like this:

  const connection = await ChannelEntity.knexLoader(viewerContext, queryContext).loadPageAsync({
    first,
    after,
    where: sql`filter_branch_id(${entityField('branchMapping')}) @>
  ${JSON.stringify(branch.getID())}::JSONB`,
    pagination: {
      strategy: PaginationStrategy.STANDARD,
      orderBy: [{ fieldName: 'createdAt', order: OrderByOrdering.DESCENDING }],
    },
  });

Also if you could add a comment saying to only use this internally in the graphql api, that'd be great:

  """
  INTERNAL USE ONLY. Channels whose branchMapping references this branch.
  Branch -> channels is not a first-class relationship in EAS Update; do not
  depend on this in public integrations.
  """

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