Skip to content

perf(yet_another_json_isolate,postgrest,functions)!: encode request bodies to UTF-8 bytes on the calling isolate in slices - #2057

Merged
spydon merged 1 commit into
mainfrom
lukasklingsbo/sdk-2401-perfpostgrestyet_another_json_isolate-encode-request-bodies
Oct 26, 2026
Merged

spydon merged 1 commit into
mainfrom
lukasklingsbo/sdk-2401-perfpostgrestyet_another_json_isolate-encode-request-bodies

Conversation

@spydon

@spydon spydon commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Large inserts were slower in v3 than in v2, and moving their encoding to an isolate did not take the work off the calling isolate. Isolate.run copies the whole payload on the calling isolate before encoding it, and the copy grows with the payload. On top of that, every client encoded the body twice: to a JSON string, then to UTF-8 when the string was assigned to request.body.

This encodes request bodies straight to UTF-8 bytes on the calling isolate. A top-level list, such as the rows of a bulk insert, is encoded in batches that yield to the event loop every 2 ms, so a frame is never blocked for long however large the payload is. Decoding is unchanged and still runs on short-lived isolates.

Changes

  • AsyncJsonCodec gains encodeBytes, the counterpart of decodeBytes. Both encode methods may now read the value until their future completes.
  • YAJsonIsolate encodes on the calling isolate, slicing a top-level list. The isolate encode path and the pass that estimated the payload size beforehand are removed. Web uses the same encoder, so it no longer blocks for the whole encode either.
  • PostgrestBuilder sends the body with request.bodyBytes. The content type stays application/json.
  • FunctionsClient sends a body with a JSON content type as bytes. Any other content type still goes through the string path, so a caller-provided charset such as iso-8859-1 keeps working.

Breaking change

An AsyncJsonCodec implementation has to add encodeBytes. AsyncJsonCodec is new in v3, so this only affects the v3 pre-releases.

Measurements

From a benchmark of the client itself, using a canned HTTP transport, compiled ahead of time on macOS arm64. "Frame blocked" is the median of the longest time per request in which the calling isolate could not run a timer.

Insert v2 v3 before v3 after
1000 rows 586 µs 834 µs 593 µs
10000 rows 5.67 ms 10.78 ms 7.20 ms
10000 rows, frame blocked 5.67 ms 5.42 ms 4.24 ms

Together with SDK-2402, which stops wrapping every inserted row in a cast view, the 10000-row insert takes 5.48 ms and blocks a frame for 2.90 ms. Reads are unchanged.

Closes SDK-2401

Summary by CodeRabbit

  • New Features
    • Added UTF-8 byte output for JSON encoding, allowing request bodies to be sent without first creating a string.
    • Large top-level lists are encoded in slices that yield to the event loop, helping keep the app responsive during encoding.
  • Documentation
    • Clarified that values passed to asynchronous encoding must not be changed until encoding completes.
    • Updated guidance on how large payloads are decoded across platforms.

…odies to UTF-8 bytes on the calling isolate in slices
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 9edc808 and 2b4aaf7.

Files selected for processing: 16
  • packages/postgrest/lib/src/postgrest_builder.dart
  • packages/postgrest/test/json_codec_test.dart
  • packages/supabase_flutter/test/async_json_codec_export_test.dart
  • packages/supabase_flutter/test/initialization_test.dart
  • packages/supabase_functions/lib/src/functions_client.dart
  • packages/supabase_functions/test/functions_dart_test.dart
  • packages/yet_another_json_isolate/README.md
  • packages/yet_another_json_isolate/lib/src/async_json_codec.dart
  • packages/yet_another_json_isolate/lib/src/json_isolate_io.dart
  • packages/yet_another_json_isolate/lib/src/json_isolate_web.dart
  • packages/yet_another_json_isolate/lib/src/sliced_json_encoder.dart
  • packages/yet_another_json_isolate/lib/yet_another_json_isolate.dart
  • packages/yet_another_json_isolate/pubspec.yaml
  • packages/yet_another_json_isolate/test/yet_another_json_isolate_io_test.dart
  • packages/yet_another_json_isolate/test/yet_another_json_isolate_test.dart
  • sdk-compliance.yaml

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

Walkthrough

The async JSON codec adds UTF-8 byte encoding and sliced encoding for large top-level lists. PostgREST and Functions request handling use byte encoding for applicable JSON request bodies.

Changes: JSON codec and sliced encoding

Layer / File(s) Summary
Codec contract and sliced encoders
packages/yet_another_json_isolate/lib/src/async_json_codec.dart, packages/yet_another_json_isolate/lib/src/sliced_json_encoder.dart, packages/yet_another_json_isolate/test/*, packages/yet_another_json_isolate/pubspec.yaml, sdk-compliance.yaml
AsyncJsonCodec adds encodeBytes and requires callers to keep input unchanged until encoding completes. String and byte encoders batch top-level lists longer than 8 elements, adjust batch sizes toward 500 microseconds, and yield between batches when a slice reaches 2,000 microseconds. Tests cover output, errors, and yielding.
Platform codec implementation
packages/yet_another_json_isolate/lib/src/json_isolate_*, packages/yet_another_json_isolate/lib/yet_another_json_isolate.dart, packages/yet_another_json_isolate/README.md, packages/yet_another_json_isolate/test/*, sdk-compliance.yaml
IO and web implementations expose encodeBytes and use the sliced encoders for string and byte output. Documentation distinguishes sliced list encoding from decoding behavior. Tests cover lifecycle and encoding behavior.
Request body encoding integration
packages/postgrest/lib/src/postgrest_builder.dart, packages/postgrest/test/json_codec_test.dart, packages/supabase_functions/lib/src/functions_client.dart, packages/supabase_functions/test/functions_dart_test.dart, packages/supabase_flutter/test/*
PostgREST sends encoded write bodies through bodyBytes. Functions uses byte encoding for JSON requests with UTF-8 encoding, and string encoding for other media types or encodings. Codec test implementations add encodeBytes.

Priority: Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant FunctionsClient
  participant AsyncJsonCodec
  participant HTTPRequest
  Caller->>FunctionsClient: invoke with request body
  alt JSON media type and UTF-8 encoding
    FunctionsClient->>AsyncJsonCodec: encodeBytes(value)
    AsyncJsonCodec-->>FunctionsClient: UTF-8 JSON bytes
  else Other media type or encoding
    FunctionsClient->>AsyncJsonCodec: encode(value)
    AsyncJsonCodec-->>FunctionsClient: JSON string
  end
  FunctionsClient->>HTTPRequest: send encoded request body
Loading
sequenceDiagram
  participant Caller
  participant PostgRESTBuilder
  participant AsyncJsonCodec
  participant HTTPRequest
  Caller->>PostgRESTBuilder: build write request
  PostgRESTBuilder->>AsyncJsonCodec: encodeBytes(value)
  AsyncJsonCodec-->>PostgRESTBuilder: UTF-8 JSON bytes
  PostgRESTBuilder->>HTTPRequest: set bodyBytes
Loading
Merge Risk: Minimal · up to 2b4aa

The reviewed encoding changes are mergeable after normal checks; no actionable risk remains.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title uses the conventional type(scope)! prefix and names the main change: request bodies are now encoded to UTF-8 bytes on the calling isolate in slices. The changeset matches this. It adds encod…
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title uses the conventional type(scope)! prefix and names the main change: request bodies are now encoded to UTF-8 bytes on the calling isolate in slices. The changeset matches this. It adds encodeBytes and sliced encoders in yet_another_json_isolate, and it switches PostgrestBuilder and FunctionsClient to send bodyBytes for JSON requests. It also removes the isolate-based encode path. The title is specific and accurate. Its length of 122 characters is longer than ideal, and the scope lists three packages.

Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (16 skipped: 16 unsupported.)

Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@spydon
spydon merged commit 776b9f5 into main Oct 26, 2026
49 checks passed
@spydon
spydon deleted the lukasklingsbo/sdk-2401-perfpostgrestyet_another_json_isolate-encode-request-bodies branch October 26, 2026 13:25
spydon added a commit that referenced this pull request Oct 26, 2026
…ast view (#2058)

Stacked on #2057.

Fixes
[SDK-2402](https://linear.app/supabase/issue/SDK-2402/perfpostgrest-stop-wrapping-every-inserted-and-upserted-row-in-a-cast).

## Summary

`_jsonRow`, which `insert()` and `upsert()` (and the typed
`insertAll`/`upsertAll`) use to turn each row of a list into a JSON
object, ended with `json.cast()`. `Map.cast` always returns a wrapping
view, even when the row already is a `Map<String, dynamic>`, so the key
check, the `columns` collection, `jsonEncode` and the copy of the body
to the encoding isolate all went through a type-checking view.

Rows that already are a `Map<String, dynamic>` are now returned as they
are. Other maps with only String keys are copied once with `Map.from`. A
row with a non-String key still throws `ArgumentError`.

Measured with a canned HTTP transport, AOT compiled, on macOS arm64:

| Insert, v3 default codec | Before | After |
| --- | --: | --: |
| 10000 rows, total | 10.78 ms | 8.25 ms |
| 10000 rows, longest main isolate stall | 5.42 ms | 2.59 ms |
| 1000 rows, total | 834 µs | 776 µs |

## Tests

`insert()` and `upsert()` of a list holding a `Map<dynamic, dynamic>`
and a `Map<String, Object?>` send the right body and `columns`
parameter, and a row with a non-String key throws.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Insert and upsert requests now correctly serialize rows provided as
maps with string keys, including maps using different value types.
  * Request columns are correctly included based on the row keys.
* Rows with non-string keys continue to be rejected with an
`ArgumentError`.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

1 participant