From 291d8cbe9b908ced8f3e6698afa079ae2a00fe0a Mon Sep 17 00:00:00 2001
From: Lukas Klingsbo
Date: Thu, 20 Aug 2026 12:34:48 +0200
Subject: [PATCH 1/5] perf(yet_another_json_isolate): process small payloads
inline and large ones on short-lived isolates
---
.../postgrest/lib/src/postgrest_builder.dart | 4 +-
.../supabase/lib/src/supabase_client.dart | 5 +-
.../lib/src/functions_client.dart | 5 +-
packages/yet_another_json_isolate/README.md | 13 +-
.../yet_another_json_isolate_benchmark.dart | 306 ++++++++++++++++++
.../lib/src/_isolates_io.dart | 229 ++++++-------
.../lib/src/_isolates_web.dart | 15 +
.../lib/yet_another_json_isolate.dart | 5 +-
.../yet_another_json_isolate/pubspec.yaml | 5 +-
.../test/yet_another_json_isolate_test.dart | 99 ++++++
sdk-compliance.yaml | 1 +
11 files changed, 546 insertions(+), 141 deletions(-)
create mode 100644 packages/yet_another_json_isolate/benchmark/yet_another_json_isolate_benchmark.dart
diff --git a/packages/postgrest/lib/src/postgrest_builder.dart b/packages/postgrest/lib/src/postgrest_builder.dart
index 14a04efe1..f904811c7 100644
--- a/packages/postgrest/lib/src/postgrest_builder.dart
+++ b/packages/postgrest/lib/src/postgrest_builder.dart
@@ -411,8 +411,8 @@ class PostgrestBuilder implements Future {
} else {
try {
final isolate = _isolate;
- if ((response.contentLength ?? 0) > 10000 && isolate != null) {
- body = await isolate.decode(response.body);
+ if (isolate != null) {
+ body = await isolate.decodeBytes(response.bodyBytes);
} else {
body = jsonDecode(response.body);
}
diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart
index 949b20ec0..f6416f675 100644
--- a/packages/supabase/lib/src/supabase_client.dart
+++ b/packages/supabase/lib/src/supabase_client.dart
@@ -40,8 +40,9 @@ import 'trace_http_client.dart';
/// if this is not supported by the client libraries. When set, the `auth`
/// namespace of the Supabase client cannot be used.
///
-/// Pass an instance of `YAJsonIsolate` to [isolate] to use your own persisted
-/// isolate instance. A new instance will be created if [isolate] is omitted.
+/// Pass an instance of `YAJsonIsolate` to [isolate] to share one instance
+/// for JSON encoding and decoding across clients. A new instance will be
+/// created if [isolate] is omitted.
///
/// The pkce flow is used by default and keeps its code verifiers in the
/// `AuthAsyncStorage` passed to the `pkceAsyncStorage` field of [authOptions].
diff --git a/packages/supabase_functions/lib/src/functions_client.dart b/packages/supabase_functions/lib/src/functions_client.dart
index 71431775a..6dc54a8f4 100644
--- a/packages/supabase_functions/lib/src/functions_client.dart
+++ b/packages/supabase_functions/lib/src/functions_client.dart
@@ -220,17 +220,16 @@ class FunctionsClient {
if (bodyBytes.isEmpty) {
data = "";
} else {
- final bodyText = utf8.decode(bodyBytes);
dynamic decoded;
try {
- decoded = await _isolate.decode(bodyText);
+ decoded = await _isolate.decodeBytes(bodyBytes);
} on FormatException {
// A body labeled JSON that doesn't parse is only tolerated on an
// error status, where the raw text still needs to reach the caller
// as the exception `details`. On a success status it's a real
// anomaly, so keep surfacing it instead of handing back a String.
if (isSuccessStatus) rethrow;
- decoded = bodyText;
+ decoded = utf8.decode(bodyBytes);
}
data = decoded;
}
diff --git a/packages/yet_another_json_isolate/README.md b/packages/yet_another_json_isolate/README.md
index 1ab2c6554..d1981a975 100644
--- a/packages/yet_another_json_isolate/README.md
+++ b/packages/yet_another_json_isolate/README.md
@@ -7,7 +7,7 @@
yet_another_json_isolate
- Simplify and improve JSON parsing in isolates by keeping one isolate running per instance.
+ JSON parsing that never blocks the main isolate for long: small payloads are processed inline, large payloads on short lived isolates that hand their result back without copying.
@@ -21,15 +21,18 @@
## Usage
```dart
-// initialize an `YAJsonIsolate` instance
-final isolate = YAJsonIsolate()..initialize();
+final isolate = YAJsonIsolate();
-// serialize a JSON using an isolate
+// serialize to a JSON string
final requestBody = await isolate.encode(requestObject);
-// deserialize a JSON string using an isolate
+// deserialize a JSON string
final json = await isolate.decode(responseBody);
+// deserialize UTF-8 encoded JSON bytes, such as an HTTP response body,
+// without materializing the intermediate string on the calling isolate
+final data = await isolate.decodeBytes(response.bodyBytes);
+
// dispose when no longer needed
isolate.dispose();
```
diff --git a/packages/yet_another_json_isolate/benchmark/yet_another_json_isolate_benchmark.dart b/packages/yet_another_json_isolate/benchmark/yet_another_json_isolate_benchmark.dart
new file mode 100644
index 000000000..c4e555379
--- /dev/null
+++ b/packages/yet_another_json_isolate/benchmark/yet_another_json_isolate_benchmark.dart
@@ -0,0 +1,306 @@
+/// Benchmarks [YAJsonIsolate] with payload shapes similar to Supabase API
+/// responses: lists of row objects at various sizes, plus concurrent load.
+///
+/// Reports per-operation latency and the longest main-isolate event-loop
+/// stall observed during each scenario, so that improvements in throughput
+/// that come at the cost of blocking the main isolate are visible.
+///
+/// Run with: dart run benchmark/yet_another_json_isolate_benchmark.dart
+library;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:yet_another_json_isolate/yet_another_json_isolate.dart';
+
+Future main() async {
+ stdout.writeln('Dart ${Platform.version}');
+ stdout.writeln('');
+
+ final singleRow = jsonEncode(_buildRow(0));
+ final kilobytes2 = _buildJsonListOfApproximateSize(2 * 1024);
+ final kilobytes50 = _buildJsonListOfApproximateSize(50 * 1024);
+ final megabytes1 = _buildJsonListOfApproximateSize(1024 * 1024);
+ final megabytes5 = _buildJsonListOfApproximateSize(5 * 1024 * 1024);
+
+ final results = <_ScenarioResult>[];
+
+ for (final scenario in [
+ _decodeScenario(
+ 'decode single row (${_formatSize(singleRow.length)})',
+ singleRow,
+ 400,
+ ),
+ _decodeScenario(
+ 'decode ${_formatSize(kilobytes2.length)}',
+ kilobytes2,
+ 400,
+ ),
+ _decodeScenario(
+ 'decode ${_formatSize(kilobytes50.length)}',
+ kilobytes50,
+ 200,
+ ),
+ _decodeScenario('decode ${_formatSize(megabytes1.length)}', megabytes1, 40),
+ _decodeScenario('decode ${_formatSize(megabytes5.length)}', megabytes5, 10),
+ _encodeScenario(
+ 'encode single row (${_formatSize(singleRow.length)})',
+ singleRow,
+ 400,
+ ),
+ _encodeScenario(
+ 'encode ${_formatSize(kilobytes2.length)}',
+ kilobytes2,
+ 400,
+ ),
+ _encodeScenario(
+ 'encode ${_formatSize(kilobytes50.length)}',
+ kilobytes50,
+ 200,
+ ),
+ _encodeScenario('encode ${_formatSize(megabytes1.length)}', megabytes1, 40),
+ _decodeBytesScenario(
+ 'decodeBytes ${_formatSize(kilobytes50.length)}',
+ kilobytes50,
+ 200,
+ ),
+ _decodeBytesScenario(
+ 'decodeBytes ${_formatSize(megabytes1.length)}',
+ megabytes1,
+ 40,
+ ),
+ _decodeBytesScenario(
+ 'decodeBytes ${_formatSize(megabytes5.length)}',
+ megabytes5,
+ 10,
+ ),
+ _concurrentDecodeScenario(
+ 'decode ${_formatSize(kilobytes50.length)} x8 concurrent',
+ kilobytes50,
+ 8,
+ 50,
+ ),
+ _concurrentDecodeScenario(
+ 'decode ${_formatSize(megabytes1.length)} x4 concurrent',
+ megabytes1,
+ 4,
+ 15,
+ ),
+ ]) {
+ results.add(await _runScenario(scenario));
+ }
+
+ _printTable(results);
+}
+
+class _Scenario {
+ _Scenario({
+ required this.name,
+ required this.iterations,
+ required this.body,
+ });
+
+ final String name;
+ final int iterations;
+ final Future Function(YAJsonIsolate isolate) body;
+}
+
+class _ScenarioResult {
+ _ScenarioResult({
+ required this.name,
+ required this.latenciesMicroseconds,
+ required this.maxStallMicroseconds,
+ });
+
+ final String name;
+ final List latenciesMicroseconds;
+ final int maxStallMicroseconds;
+
+ int get median => _percentile(0.5);
+ int get percentile90 => _percentile(0.9);
+ int get maximum => latenciesMicroseconds.last;
+
+ int _percentile(double fraction) {
+ final index = ((latenciesMicroseconds.length - 1) * fraction).round();
+ return latenciesMicroseconds[index];
+ }
+}
+
+_Scenario _decodeScenario(String name, String json, int iterations) {
+ return _Scenario(
+ name: name,
+ iterations: iterations,
+ body: (isolate) => isolate.decode(json),
+ );
+}
+
+_Scenario _decodeBytesScenario(String name, String json, int iterations) {
+ final bytes = utf8.encode(json);
+ return _Scenario(
+ name: name,
+ iterations: iterations,
+ body: (isolate) => isolate.decodeBytes(bytes),
+ );
+}
+
+_Scenario _encodeScenario(String name, String json, int iterations) {
+ final value = jsonDecode(json);
+ return _Scenario(
+ name: name,
+ iterations: iterations,
+ body: (isolate) => isolate.encode(value),
+ );
+}
+
+_Scenario _concurrentDecodeScenario(
+ String name,
+ String json,
+ int concurrency,
+ int iterations,
+) {
+ return _Scenario(
+ name: name,
+ iterations: iterations,
+ body: (isolate) => Future.wait(
+ [for (var i = 0; i < concurrency; i++) isolate.decode(json)],
+ ),
+ );
+}
+
+Future<_ScenarioResult> _runScenario(_Scenario scenario) async {
+ final isolate = YAJsonIsolate(debugName: 'benchmark');
+ await isolate.initialize();
+
+ final warmupIterations = (scenario.iterations ~/ 10).clamp(2, 20);
+ for (var i = 0; i < warmupIterations; i++) {
+ await scenario.body(isolate);
+ }
+
+ final latencies = [];
+ final stallMonitor = _EventLoopStallMonitor()..start();
+ final stopwatch = Stopwatch();
+ for (var i = 0; i < scenario.iterations; i++) {
+ stopwatch
+ ..reset()
+ ..start();
+ await scenario.body(isolate);
+ stopwatch.stop();
+ latencies.add(stopwatch.elapsedMicroseconds);
+ // Yields to the event queue so the stall monitor's timer gets a chance
+ // to fire between operations; awaiting only the operation would starve
+ // it and hide stalls caused by synchronous work.
+ await Future.delayed(Duration.zero);
+ }
+ final maxStall = stallMonitor.stop();
+ await isolate.dispose();
+
+ latencies.sort();
+ stdout.writeln('finished: ${scenario.name}');
+ return _ScenarioResult(
+ name: scenario.name,
+ latenciesMicroseconds: latencies,
+ maxStallMicroseconds: maxStall,
+ );
+}
+
+/// Measures gaps between 1ms periodic timer ticks on the main isolate.
+///
+/// A gap far above 1ms means the main isolate was blocked and would have
+/// dropped frames in a Flutter application.
+class _EventLoopStallMonitor {
+ final Stopwatch _stopwatch = Stopwatch();
+ Timer? _timer;
+ int _previousTickMicroseconds = 0;
+ int _maxGapMicroseconds = 0;
+
+ void start() {
+ _stopwatch
+ ..reset()
+ ..start();
+ _previousTickMicroseconds = 0;
+ _maxGapMicroseconds = 0;
+ _timer = Timer.periodic(const Duration(milliseconds: 1), (_) {
+ final now = _stopwatch.elapsedMicroseconds;
+ final gap = now - _previousTickMicroseconds;
+ if (gap > _maxGapMicroseconds) {
+ _maxGapMicroseconds = gap;
+ }
+ _previousTickMicroseconds = now;
+ });
+ }
+
+ /// Stops the monitor and returns the longest stall in microseconds, with
+ /// the expected 1ms tick interval subtracted.
+ int stop() {
+ _timer?.cancel();
+ _stopwatch.stop();
+ final stall = _maxGapMicroseconds - 1000;
+ return stall < 0 ? 0 : stall;
+ }
+}
+
+Map _buildRow(int index) {
+ return {
+ 'id': index,
+ 'uuid': '00000000-0000-4000-8000-${index.toString().padLeft(12, '0')}',
+ 'created_at': '2026-08-20T09:00:00.000Z',
+ 'name': 'user_$index',
+ 'email': 'user_$index@example.com',
+ 'is_active': index.isEven,
+ 'score': index * 1.5,
+ 'tags': ['alpha', 'beta', 'gamma'],
+ 'metadata': {
+ 'source': 'benchmark',
+ 'index': index,
+ 'nested': {'depth': 2, 'flag': true},
+ },
+ 'description':
+ 'Row $index with a description long enough to resemble real user '
+ 'generated content stored in a text column of a Supabase table.',
+ };
+}
+
+String _buildJsonListOfApproximateSize(int targetBytes) {
+ final rows =