Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Jint.Benchmark/StringSliceBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ namespace Jint.Benchmark;

/// <summary>
/// Isolates String.prototype.slice/substring/substr cost over a large (128K char) string —
/// the dromaeo-object-string shape where large slice results are assigned and discarded
/// (measured: slice = 89.9%, substring = 9.2% of its 1.26 GB/op allocations).
/// the dromaeo-object-string shape where large slice results are assigned and discarded.
/// Both SliceLargeDiscard and SubstringLargeDiscard use the actual dromaeo arguments
/// (start, -1): substring clamps the -1 to 0 and swaps, producing a 12000-char result that
/// previously fell below the zero-copy retention guard and copied on every call.
/// SliceSmall guards the small-result path; SliceThenRead guards lazy-materialization cost
/// when the result is actually consumed.
/// </summary>
Expand Down Expand Up @@ -39,7 +41,7 @@ public void Setup()
var ret = null;
for (var i = 0; i < 5000; i++) {
ret = str.substring(0);
ret = str.substring(12000, str.length - 1);
ret = str.substring(12000, -1);
}
return ret.length;
})();
Expand Down
19 changes: 13 additions & 6 deletions Jint/Native/JsString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,18 @@ internal static JsString Create(char value)
return new JsString(value);
}

// A zero-copy view pins the whole source string, so a slice only becomes a view when retention
// stays bounded: either it covers at least half the source (≤ 2× the result is pinned), or the
// bytes it pins but never uses (source.Length - length) stay within this fixed budget. The latter
// catches a moderate slice of a large source — e.g. substring(12000, -1) of a ~128K string, the
// dromaeo-object-string shape — which otherwise copies on every call.
private const int SliceViewMaxWastedChars = 128 * 1024;

/// <summary>
/// Creates a string for a substring of <paramref name="source"/>. Large slices that cover
/// most of the source are returned as zero-copy views (<see cref="SlicedString"/>); small
/// slices are copied so a short-lived view can never pin a much larger backing string.
/// Creates a string for a substring of <paramref name="source"/>. Large slices that cover most of
/// the source, or moderate slices whose unused pinned remainder stays within
/// <see cref="SliceViewMaxWastedChars"/>, are returned as zero-copy views (<see cref="SlicedString"/>);
/// smaller slices are copied so a short-lived view can never pin a much larger backing string.
/// </summary>
internal static JsString CreateSliced(string source, int start, int length)
{
Expand All @@ -215,9 +223,8 @@ internal static JsString CreateSliced(string source, int start, int length)
return new JsString(source);
}

// The view pins the whole source string; only worth it (and safe retention-wise)
// when the slice is large and covers at least half of the source.
if (length >= 512 && length * 2 >= source.Length)
if (length >= 512
&& (length * 2 >= source.Length || source.Length - length <= SliceViewMaxWastedChars))
{
return new SlicedString(source, start, length);
}
Expand Down
Loading