perf: Replace java.net.URI with custom string parsing in Dsn#5448
perf: Replace java.net.URI with custom string parsing in Dsn#5448runningcode wants to merge 3 commits into
Conversation
The Dsn constructor used `new URI(dsnString).normalize()` to parse the DSN string, which is known to be slow on Android. Since `retrieveParsedDsn()` is called on the main thread during `Sentry.init()` via `preInitConfigurations()`, this directly impacts app startup time. Replace the URI-based parsing with manual indexOf/substring operations. The only remaining URI construction is from pre-parsed components (`new URI(scheme, null, host, port, path, null, null)`), which is significantly cheaper since the JDK doesn't need to re-parse a string. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover edge cases specific to the manual indexOf/substring parser: null input, missing scheme separator, no slash after host, multiple path segments, port with path, multiple double slashes, query string with port, empty secret key, and a realistic Sentry DSN with org id. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 27d7cf8 | 309.43 ms | 364.27 ms | 54.85 ms |
| ee35ac3 | 346.83 ms | 435.48 ms | 88.65 ms |
| 2124a46 | 319.19 ms | 415.04 ms | 95.85 ms |
| f6cdbf0 | 314.19 ms | 357.59 ms | 43.40 ms |
| 22f4345 | 325.23 ms | 454.66 ms | 129.43 ms |
| fcec2f2 | 357.47 ms | 447.32 ms | 89.85 ms |
| f064536 | 329.00 ms | 395.62 ms | 66.62 ms |
| bb0ff41 | 344.70 ms | 413.82 ms | 69.12 ms |
| d15471f | 361.89 ms | 378.07 ms | 16.18 ms |
| e59e22a | 368.02 ms | 432.00 ms | 63.98 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 27d7cf8 | 1.58 MiB | 2.12 MiB | 549.42 KiB |
| ee35ac3 | 1.58 MiB | 2.13 MiB | 558.77 KiB |
| 2124a46 | 1.58 MiB | 2.12 MiB | 551.51 KiB |
| f6cdbf0 | 0 B | 0 B | 0 B |
| 22f4345 | 1.58 MiB | 2.29 MiB | 719.83 KiB |
| fcec2f2 | 1.58 MiB | 2.12 MiB | 551.50 KiB |
| f064536 | 1.58 MiB | 2.20 MiB | 633.90 KiB |
| bb0ff41 | 0 B | 0 B | 0 B |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| e59e22a | 1.58 MiB | 2.20 MiB | 635.34 KiB |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cacc249. Configure here.
| final String hostAndPath = | ||
| queryIndex < 0 | ||
| ? dsnString.substring(hostStart) | ||
| : dsnString.substring(hostStart, queryIndex); |
There was a problem hiding this comment.
URI fragment not stripped, corrupts projectId silently
Low Severity
The new parser strips query strings (?...) but never strips URI fragments (#...). The old java.net.URI.getPath() automatically excluded fragments. If a DSN contains a # (e.g. https://key@host/123#frag), the fragment leaks into projectId (becoming 123#frag), and the multi-arg URI constructor percent-encodes # as %23, producing an incorrect sentryUri path. The queryIndex stripping logic at line 100 needs a similar check for #.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit cacc249. Configure here.
| } else { | ||
| host = hostPort.substring(0, portColon); | ||
| port = Integer.parseInt(hostPort.substring(portColon + 1)); | ||
| } |
There was a problem hiding this comment.
IPv6 host parsing broken by first-colon port detection
Low Severity
hostPort.indexOf(':') finds the first colon, which for IPv6 literals like [::1] or [::1]:9000 matches a colon inside the brackets rather than the port separator. This causes host to be [ and Integer.parseInt to throw on the remaining garbage. The old java.net.URI parser handled IPv6 bracket syntax correctly. Self-hosted Sentry users with IPv6 addresses would be unable to initialize the SDK.
Reviewed by Cursor Bugbot for commit cacc249. Configure here.
0xadam-brown
left a comment
There was a problem hiding this comment.
A few optionals; otherwise lgtm
| // Avoids java.net.URI for DSN parsing, which is slow on Android. | ||
| Dsn(@Nullable String dsn) throws IllegalArgumentException { | ||
| try { | ||
| final String dsnString = Objects.requireNonNull(dsn, "The DSN is required.").trim(); |
There was a problem hiding this comment.
l: Thoughts about extracting logic into static methods for legibility? Something like:
try {
final String s = requireValid(dsn);
final String scheme = extractScheme(s);
final String userInfo = extractUserInfo(s);
final int keyColon = userInfo.indexOf(':');
this.publicKey = keyColon < 0 ? userInfo : userInfo.substring(0, keyColon);
this.secretKey = keyColon < 0 ? null : userInfo.substring(keyColon + 1);
if (this.publicKey.isEmpty()) {
throw new IllegalArgumentException("Invalid DSN: No public key provided.");
}
final String hostPort = extractHostPort(s);
final int portColon = hostPort.indexOf(':');
final String host = portColon < 0 ? hostPort : hostPort.substring(0, portColon);
final int port = portColon < 0 ? -1 : Integer.parseInt(hostPort.substring(portColon + 1));
final String rawPath = extractNormalizedPath(s);
this.path = extractPathSegment(rawPath);
final String projectId = extractProjectId(rawPath);
this.projectId = projectId;
this.sentryUri = buildSentryUri(scheme, host, port, this.path, projectId);
this.orgId = extractOrgId(host);
} catch (Throwable e) {
throw new IllegalArgumentException(e);
}Claude thinks HotSpot, dex2oat, etc. would inline the invocations, meaning no practical overhead.
Up to you, of course.
| // Extract host, optional port, and path+projectId | ||
| final int hostStart = atIndex + 1; | ||
|
|
||
| // Strip query string if present |
There was a problem hiding this comment.
Strip any fragment too?
final int fragmentIndex = dsnString.indexOf('#', hostStart);URI would've done it. Not sure how much we care about saving folks from obvious errors they're unlikely to make, but including it will keep parity.
There was a problem hiding this comment.
...ah, cursorbot beat me to it.
| port = -1; | ||
| } else { | ||
| host = hostPort.substring(0, portColon); | ||
| port = Integer.parseInt(hostPort.substring(portColon + 1)); |
There was a problem hiding this comment.
l: We could give a more informative error message if the port is malformed / not an int:
final String portStr = hostPort.substring(portColon + 1);
try {
port = Integer.parseInt(portStr);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid DSN: invalid port: " + portStr);
}(Presumably rare in practice, and cleaner to do if we extract logic into static methods. Also up to you.)
|
|
||
| @Test | ||
| fun `when dsn is null, throws exception`() { | ||
| assertFailsWith<IllegalArgumentException> { Dsn(null) } |
There was a problem hiding this comment.
l: My vote would be for asserting particular error messages in these tests, as they're part of our implicit API. (Looks like we sometimes do it above.)


Summary
new URI(dsnString).normalize()in theDsnconstructor with manualindexOf/substringparsing to avoid the slowjava.net.URIparser on AndroidretrieveParsedDsn()is called on the main thread duringSentry.init()→preInitConfigurations(), so this directly impacts app startup timeURIconstruction is from pre-parsed components (new URI(scheme, null, host, port, path, null, null)), which is much cheaper since it doesn't re-parse a stringRelates to: https://linear.app/getsentry/issue/JAVA-516/move-dsn-parsing-off-the-main-thread-during-init
🤖 Generated with Claude Code