Skip to content

[build-tools] - Gradle build profile report with task execution breakdown#3629

Merged
AbbanMustafa merged 12 commits into
mainfrom
mus/gradle-profiler
May 19, 2026
Merged

[build-tools] - Gradle build profile report with task execution breakdown#3629
AbbanMustafa merged 12 commits into
mainfrom
mus/gradle-profiler

Conversation

@AbbanMustafa
Copy link
Copy Markdown
Contributor

@AbbanMustafa AbbanMustafa commented Apr 24, 2026

Why

We want to give developers visibility into which Gradle tasks are consuming the most build time. This helps them and us identify optimization opportunities, especially when evaluating Gradle caching effectiveness and which modules we can improve.

How

  • Added --profile to the gradlew command, which tells Gradle to generate an HTML profile report. We parse this report and display the data in a build phase, broken down by module.
  • Added parseGradleProfile() which reads the HTML report and extracts task path, duration, and result from the Task Execution table
  • Added a GRADLE_BUILD_PROFILE build phase that logs a formatted table with:
    • Tasks grouped by module, with individual tasks nested underneath using tree characters
    • Duration, % of total time, result (executed/up-to-date/skipped/etc.), and a proportional bar chart
    • Tasks under 1 second filtered out to reduce noise
    • Summary line with total task count, cached/up-to-date count, and total time
  • Removed the previous custom trace init script and trace parsing/upload plumbing
  • Simplified BuildResult to return Artifacts directly since gradleProfileTasks is no longer passed up
  • Everything is gated behind EXPERIMENTAL_GRADLE_PROFILE=1 env var

Test Plan

  • Validated locally with EXPERIMENTAL_GRADLE_PROFILE=1 — profile table shows in build logs with correct task grouping and durations
  • Validated with EAS_GRADLE_CACHE=1 — module totals and executed tasks display correctly
Screenshot 2026-05-07 at 5 53 00 PM

@AbbanMustafa AbbanMustafa added the no changelog PR that doesn't require a changelog entry label Apr 27, 2026
@byCedric
Copy link
Copy Markdown
Member

byCedric commented May 7, 2026

Big fan of this, after testing Android builds in Launch and seeing it's roughly 5x as slow for the default Expo template (SDK 54) - amounting for a total of 15m12s just for the gradle step

thank-you

@codecov
Copy link
Copy Markdown

codecov Bot commented May 7, 2026

Codecov Report

❌ Patch coverage is 0% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.99%. Comparing base (10c7ac6) to head (4817368).

Files with missing lines Patch % Lines
...ages/build-tools/src/steps/utils/android/gradle.ts 0.00% 6 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3629   +/-   ##
=======================================
  Coverage   56.99%   56.99%           
=======================================
  Files         902      902           
  Lines       38874    38874           
  Branches     8127     8127           
=======================================
  Hits        22154    22154           
  Misses      15263    15263           
  Partials     1457     1457           

☔ View full report in Codecov by Sentry.
📢 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.

@AbbanMustafa AbbanMustafa changed the title [build] gradle parse profiler [build-tools] - Gradle build profile report with task execution breakdown May 7, 2026
@AbbanMustafa AbbanMustafa marked this pull request as ready for review May 7, 2026 22:25
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Subscribed to pull request

File Patterns Mentions
**/* @douglowder

Generated by CodeMention

@AbbanMustafa AbbanMustafa requested a review from sjchmiela May 8, 2026 16:01
Comment thread packages/worker/src/build.ts Outdated

analytics.logEvent(Event.WORKER_BUILD_SUCCESS, {});

if (job.platform === Platform.ANDROID && ctx.env.EXPERIMENTAL_GRADLE_PROFILE === '1') {
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.

i've been thinking we may not need to prefix flags with EXPERIMENTAL -- until we document flags i think we can consider them experimental?

Comment thread packages/worker/src/build.ts Outdated
if (profileTasks && profileTasks.length > 0) {
const report = formatGradleProfileReport(profileTasks);
for (const line of report.split('\n')) {
logger.info(line);
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.

we could also logger.info(report)? or no?

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.

bunyan escapes newlines, it will all log on one line.

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.

what i meant is that i think we might be ok with this looking like this (package.json has newlines and prints in one… log entry)

Zrzut ekranu 2026-05-18 o 17 53 57

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.

ok if thats fine, we can do that

env: { ...env, ...extraEnv, LC_ALL: 'C.UTF-8' },
});

const profileFlag = env['EXPERIMENTAL_GRADLE_PROFILE'] === '1' ? '--profile' : '';
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.

does this impact build performance? could we add it to all builds?

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.

it does not, yes we can and would be nice to enable by default.

result: string;
}

export async function parseGradleProfile(
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.

let's have a separate utility for parsing gradle profiles?

const html = await fs.readFile(path.join(profileDir, htmlFile), 'utf8');

// Find the "Task Execution" section - it's the last <table> in the "Task Execution" tab
const taskExecMatch = html.match(/<h2[^>]*>\s*Task Execution\s*<\/h2>([\s\S]*?)(?:<\/div>)/i);
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.

wouldn't it be nicer to parse the profile as an xml?

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.

thanks I moved over to using fast-xml-parser!

const nameWidth = Math.max(4, ...rows.map(r => r.displayName.length)) + 2;
const barMaxWidth = 20;

const header =
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.

@hSATAC was formatting a table manually for xclogparsing too… i wonder if there's some kind of utility we could pull in / abstract some logic…

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.

I agree. Will most likely prefer to keep it this way for now and then we can later agree how we would like to parse and show these in the UI

Comment thread packages/worker/src/build.ts Outdated
Comment on lines +96 to +101
const androidDir = path.join(ctx.getReactNativeProjectDirectory(), 'android');
const profileTasks = await parseGradleProfile(androidDir, logger);
if (profileTasks && profileTasks.length > 0) {
const report = formatGradleProfileReport(profileTasks);
logger.info(report);
}
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.

shall we wrap with try-catch so if something fails here we can log the error and mark the phase as skipped? bonus points for logging the error with sentry

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.

haven't looked it yet but i bet parseGradleProfile is probably a big try-catch, but i think maybe it's nicer to have the caller catch the error?

const html = await fs.readFile(path.join(profileDir, htmlFile), 'utf8');

// Locate the <h2>Task Execution</h2> heading and extract its <table>
const headingMatch = html.match(/<h2[^>]*>\s*Task Execution\s*<\/h2>/i);
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.

ugh i thought fast-xml-parser would give us what cheerio would do…

i'll let you decide what to do

.pop();

if (!htmlFile) {
logger?.info('No Gradle profile HTML found in %s', profileDir);
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.

so if we allowed parseGradleProfile to throw we wouldn't need to pass in logger and optionally call it. instead we could throw SystemError and catch it and log it once


const tasks: GradleProfileTask[] = [];
for (const row of rows) {
const cells: any[] = row?.td;
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.

Suggested change
const cells: any[] = row?.td;
const cells: unknown[] = row?.td;

@AbbanMustafa AbbanMustafa force-pushed the mus/gradle-profiler branch from 84ebdd6 to 4817368 Compare May 19, 2026 13:26
@github-actions
Copy link
Copy Markdown

⏩ The changelog entry check has been skipped since the "no changelog" label is present.

@AbbanMustafa AbbanMustafa merged commit ecf58ab into main May 19, 2026
11 checks passed
@AbbanMustafa AbbanMustafa deleted the mus/gradle-profiler branch May 19, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no changelog PR that doesn't require a changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants