Skip to content

perf(android): Avoid exception-driven control flow in getResourceId#5631

Open
runningcode wants to merge 2 commits into
mainfrom
no/perf-android-resource-id-exceptions
Open

perf(android): Avoid exception-driven control flow in getResourceId#5631
runningcode wants to merge 2 commits into
mainfrom
no/perf-android-resource-id-exceptions

Conversation

@runningcode

@runningcode runningcode commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Resolves JAVA-586

📜 Description

ViewUtils.getResourceId used exception-driven control flow: it threw Resources.NotFoundException for views with View.NO_ID or a generated id, and every caller caught and discarded it. This ran per view during view-hierarchy snapshots and gesture target resolution, so in Compose-heavy apps — where most views have generated ids — the SDK constructed an exception (and a native fillInStackTrace stack walk) for nearly every view, on the main thread.

This adds a non-throwing ViewUtils.resolveResourceId(View) that returns null for unresolved ids, and routes the hot callers (ViewHierarchyEventProcessor.viewToNode, AndroidViewGestureTargetLocator.createUiElement, getResourceIdWithFallback) through it. The public getResourceId(...) throws Resources.NotFoundException stays as a thin wrapper for backward compatibility. Emitted identifiers and fallbacks are unchanged.

💡 Motivation and Context

Found while analyzing a customer-provided Perfetto trace: in a single view-hierarchy snapshot, 24 of 72 getResourceId calls threw Resources.NotFoundException on the main thread — pure overhead with no functional result.

💚 How did you test it?

  • Unit tests in ViewUtilsTest cover the new resolveResourceId (generated id, NO_ID, resource-not-found, and success) plus the existing getResourceId/fallback behavior; ViewHierarchyEventProcessorTest and AndroidViewGestureTargetLocatorTest pass unchanged.

  • On-device A/B on a Pixel 3 (Android 12) via ART method tracing, analyzed in Perfetto trace_processor, over 2048 calls on NO_ID views:

    old (getResourceId) new (resolveResourceId)
    Resources$NotFoundException.<init> 2048 0
    Throwable.fillInStackTrace 2048 0
    total methods executed 30,757 12,324
    inclusive time / call (traced) 30,470 ns 6,467 ns

    Exactly one exception (+ native stack fill) per unresolved view is removed; ~60% fewer executed methods and ~4.7× cheaper per unresolved view (the absolute ns is inflated by tracing; the call counts and ratio are the reliable signal).

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

Follow-ups identified in the same trace but intentionally out of scope here: per-touch LinkedListArrayDeque in ViewUtils.findTarget, and background JSON serialization cost.

@sentry

sentry Bot commented Jun 25, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.46.0 (1) release

⚙️ sentry-android Build Distribution Settings

@linear-code

linear-code Bot commented Jun 25, 2026

Copy link
Copy Markdown

JAVA-586

@runningcode runningcode marked this pull request as ready for review June 25, 2026 11:11

@0xadam-brown 0xadam-brown 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.

Always a win! One optional nit if you want it 💯

* @param view - the view whose id is being retrieved
* @return human-readable view id, or {@code null} if it cannot be resolved
*/
public static @Nullable String resolveResourceId(final @NotNull View view) {

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.

l: Thoughts about getResourceIdOrNull() to parallel getResourceId()?

Up to you, of course...

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 idea. will add!

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.

on second though I also removed the other method that throws since it is unused, internal and not part of our public api. gonna ask for a re-review

@runningcode runningcode force-pushed the no/perf-android-resource-id-exceptions branch 2 times, most recently from eeff1d6 to 9f7943e Compare June 26, 2026 09:54
@github-actions

Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 385.98 ms 467.57 ms 81.59 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
4e3e79d 369.55 ms 418.39 ms 48.83 ms
22ff2c7 306.60 ms 336.65 ms 30.05 ms
092f017 353.13 ms 433.84 ms 80.71 ms
0eaac1e 322.53 ms 389.31 ms 66.78 ms
d15471f 322.58 ms 396.08 ms 73.50 ms
fc5ccaf 322.49 ms 405.25 ms 82.76 ms
e2dce0b 315.85 ms 369.20 ms 53.35 ms
22f4345 314.79 ms 375.02 ms 60.23 ms
9770665 315.64 ms 378.00 ms 62.36 ms
7314dbe 437.83 ms 505.64 ms 67.81 ms

App size

Revision Plain With Sentry Diff
4e3e79d 0 B 0 B 0 B
22ff2c7 0 B 0 B 0 B
092f017 0 B 0 B 0 B
0eaac1e 1.58 MiB 2.19 MiB 619.17 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
fc5ccaf 1.58 MiB 2.13 MiB 557.54 KiB
e2dce0b 0 B 0 B 0 B
22f4345 1.58 MiB 2.29 MiB 719.83 KiB
9770665 0 B 0 B 0 B
7314dbe 1.58 MiB 2.10 MiB 533.45 KiB

runningcode and others added 2 commits June 26, 2026 12:04
ViewUtils.getResourceId threw Resources.NotFoundException for views with
no id or a generated id, and callers caught and discarded it. During a
view-hierarchy snapshot and on every gesture this ran per view, so in
Compose-heavy apps where most views have generated ids the SDK
constructed an exception (and a native stack trace fill) per view on the
main thread.

Add a non-throwing resolveResourceId that returns null for unresolved
ids and route the hot callers through it. The public getResourceId
remains as a throwing wrapper for backward compatibility. Behavior
(emitted identifiers and fallbacks) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@runningcode runningcode force-pushed the no/perf-android-resource-id-exceptions branch from 9f7943e to e484785 Compare June 26, 2026 10:04
@runningcode runningcode requested a review from 0xadam-brown June 26, 2026 10:07
* @throws Resources.NotFoundException in case the view id was not found
* @return human-readable view id, or {@code null} if it cannot be resolved
*/
public static String getResourceId(final @NotNull View view) throws Resources.NotFoundException {

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.

this old method signature was removed so it is a breaking change but not part of the public api

@romtsn romtsn 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.

very nice!

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