[js_runtime] Use URLSearchParams to escape query parameters

Use URLSearchParams to escape query parameters.

- For large query parameters (>100KB), where escaping the parameters causes jank, this can be 2x-5x faster, reducing ~100ms pauses to nearer frame rate.

- For small query parameters (<100B) it can be slightly (10-20%) slower, but still well below 1 millisecond.


TEST=ci

Issue: #53712

Change-Id: I045bac7a067a658a58aaac4266409d526ccda774
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/329822
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Siva Annamalai <asiva@google.com>
Reviewed-by: Ömer Ağacan <omersa@google.com>
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Reviewed-by: Lasse Nielsen <lrn@google.com>
This commit is contained in:
Stephen Adams 2023-10-13 16:36:47 +00:00 committed by Commit Queue
parent 54b588a373
commit 8ea74c0f2e
6 changed files with 91 additions and 0 deletions

View file

@ -2448,6 +2448,7 @@ const Set<String> reservedCapitalizedGlobalSymbols = {
// Some additional names
"Isolate",
"URLSearchParams",
};
/// Symbols that we might be using in our JS snippets. Some of the symbols in

View file

@ -932,6 +932,12 @@ class _Uri {
}
return result.toString();
}
@patch
static String _makeQueryFromParameters(
Map<String, dynamic /*String?|Iterable<String>*/ > queryParameters) {
return _makeQueryFromParametersDefault(queryParameters);
}
}
@patch

View file

@ -826,6 +826,67 @@ class _Uri {
}
return result.toString();
}
@patch
static String _makeQueryFromParameters(
Map<String, dynamic /*String?|Iterable<String>*/ > queryParameters) {
if (!_useURLSearchParams) {
return _makeQueryFromParametersDefault(queryParameters);
}
// Copy the values from [queryParameters] into a browser URLSearchParams
// object.
final params = JS('', 'new URLSearchParams()');
queryParameters.forEach((key, value) {
if (value is String) {
JS('', '#.set(#, #)', params, key, value);
} else if (value == null) {
JS('', '#.set(#, #)', params, key, '');
} else {
Iterable values = value;
// This could be written as `for (final String? value in values)` but in
// some optimized modes the type check is 'trusted' (ignored). So we
// check [value] explicitly to avoid JavaScript implicit ToString
// conversions.
for (final value in values) {
if (value is String) {
JS('', '#.append(#, #)', params, key, value);
} else if (value == null) {
JS('', '#.append(#, #)', params, key, '');
} else {
// Signal the error we would have for the typed for-in loop.
value as String?;
}
}
}
});
String encoded = JS('String', '#.toString()', params);
// Fix differences between the URLSearchParams encoding and the desired
// encoding.
//
// 1. URLSearchParams encodes empty values as `foo=` rather than just `foo`.
// 2. URLSearchParams does not escape `*`.
// 3. URLSearchParams escapes `~`.
// Handle `foo=` at end of encoded query.
final length = encoded.length;
if (length > 0 && encoded[length - 1] == '=') {
encoded = encoded.substring(0, length - 1);
}
// Handle other cases with one RegExp.
encoded = JS(
'',
r'#.replace(/=&|\*|%7E/g, (m) => m === "=&" ? "&" : m === "*" ? "%2A" : "~")',
encoded);
return encoded;
}
static final bool _useURLSearchParams =
JS('bool', 'typeof URLSearchParams == "function"');
}
@patch

View file

@ -78,4 +78,10 @@ class _Uri {
}
return result.toString();
}
@patch
static String _makeQueryFromParameters(
Map<String, dynamic /*String?|Iterable<String>*/ > queryParameters) {
return _makeQueryFromParametersDefault(queryParameters);
}
}

View file

@ -70,4 +70,10 @@ class _Uri {
}
return result.toString();
}
@patch
static String _makeQueryFromParameters(
Map<String, dynamic /*String?|Iterable<String>*/ > queryParameters) {
return _makeQueryFromParametersDefault(queryParameters);
}
}

View file

@ -2350,7 +2350,18 @@ class _Uri implements Uri {
escapeDelimiters: true);
}
if (queryParameters == null) return null;
return _makeQueryFromParameters(queryParameters);
}
external static String _makeQueryFromParameters(
Map<String, dynamic /*String?|Iterable<String>*/ > queryParameters);
/// Default implementation of [_makeQueryFromParameters].
///
/// This implementation is used from the patch for [_makeQueryFromParameters]
/// where there is not a more efficient native implementation available.
static String _makeQueryFromParametersDefault(
Map<String, dynamic /*String?|Iterable<String>*/ > queryParameters) {
var result = StringBuffer();
var separator = "";