[build-tools] - Gradle build profile report with task execution breakdown#3629
Conversation
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
|
Subscribed to pull request
Generated by CodeMention |
|
|
||
| analytics.logEvent(Event.WORKER_BUILD_SUCCESS, {}); | ||
|
|
||
| if (job.platform === Platform.ANDROID && ctx.env.EXPERIMENTAL_GRADLE_PROFILE === '1') { |
There was a problem hiding this comment.
i've been thinking we may not need to prefix flags with EXPERIMENTAL -- until we document flags i think we can consider them experimental?
| if (profileTasks && profileTasks.length > 0) { | ||
| const report = formatGradleProfileReport(profileTasks); | ||
| for (const line of report.split('\n')) { | ||
| logger.info(line); |
There was a problem hiding this comment.
we could also logger.info(report)? or no?
There was a problem hiding this comment.
bunyan escapes newlines, it will all log on one line.
There was a problem hiding this comment.
ok if thats fine, we can do that
| env: { ...env, ...extraEnv, LC_ALL: 'C.UTF-8' }, | ||
| }); | ||
|
|
||
| const profileFlag = env['EXPERIMENTAL_GRADLE_PROFILE'] === '1' ? '--profile' : ''; |
There was a problem hiding this comment.
does this impact build performance? could we add it to all builds?
There was a problem hiding this comment.
it does not, yes we can and would be nice to enable by default.
| result: string; | ||
| } | ||
|
|
||
| export async function parseGradleProfile( |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
wouldn't it be nicer to parse the profile as an xml?
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
@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…
There was a problem hiding this comment.
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
| 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); | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
| const cells: any[] = row?.td; | |
| const cells: unknown[] = row?.td; |
84ebdd6 to
4817368
Compare
|
⏩ The changelog entry check has been skipped since the "no changelog" label is present. |


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
--profileto 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.parseGradleProfile()which reads the HTML report and extracts task path, duration, and result from the Task Execution tableGRADLE_BUILD_PROFILEbuild phase that logs a formatted table with:BuildResultto returnArtifactsdirectly sincegradleProfileTasksis no longer passed upEXPERIMENTAL_GRADLE_PROFILE=1env varTest Plan
EXPERIMENTAL_GRADLE_PROFILE=1— profile table shows in build logs with correct task grouping and durationsEAS_GRADLE_CACHE=1— module totals and executed tasks display correctly