Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Breaking Change Request] Deprecate UnmodifiableUint8ListView and friends #53218

Open
rakudrama opened this issue Aug 15, 2023 · 12 comments
Open
Assignees
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. breaking-change-approved breaking-change-request This tracks requests for feedback on breaking changes library-typed-data

Comments

@rakudrama
Copy link
Member

TL;DR: To ensure that typed data has reliable performance on the JavaScript platforms we need to get rid of the UnmodifiableUint8ListView and similar classes.

What?

  • Deprecate UnmodifiableUint8ListView and everything else in unmodifiable_typed_data.dart.

  • Replace the constructor for UnmodifiableUint8ListView with a new factory constructor Uint8List.unmodifiableView(Uint8List list), and likewise for the other unmodifiable views.

Why?

Uint8List is sealed to prevent poor performance, but the SDK itself is constructed with the pattern that sealing is trying to prevent - multiple implementations with non-aligned 'shapes', forcing each byte access to be a dispatched aka virtual call. The remedy is to remove the user-visible classes that have this bad pattern.

Benchmarking shows that the polymorphism costs ~10x performance on code that is capable of reading from a regular Uint8List and an UnmodifiableUit8ListView. We could recover that performance by eliminating the polymorphism.
(Comparing golem Benchmarks?benchmark=TypedDataPoly#benchmarks%3DTypedDataPoly.A_UV.array.100%2CTypedDataPoly.A_V.array.100%3Btargets%3Ddart2js-production)

In JavaScript there is a single class that can support Uint8List efficiently: Uint8Array. Having a separate UnmodifiableUint8ListView class makes the operations polymorphic and too slow - often over an order of magnitude slower. Reliable performance of typed data requires all instances of Uint8List to be implemented as Uint8Array.

It is too difficult to provide a fiction that one implementation class (JavaScript Uint8Array) conditionally implements an interface. So we need to have a single interface type in the SDK.

It might turn out that it is possible to use JavaScript subtyping by extending Uint8Array, but in the past we have found that ES6 and later features do not always work efficiently when used in their full generality. Preliminary experiments show that the problem we are trying to avoid with this breaking change request will re-assert itself at a lower level in at least one popular JavaScript engine. Removing UnmodifiableUint8ListView as a user-visible type gives us the freedom to choose the implementation strategy that works well across browsers, including possibly ignoring the checks

Impact

There would be no impact on performance the native VM implementations. (The VM implementations have already been optimized to align the private classes that implement UnmodifiableUint8ListView with those that implement writable Uint8Lists.)

Current usage of the form UnmodifiableUint8ListView(list) becomes Uint8List.unmodifiableView(list).

The unmodifiable views are fairly special purpose and only lightly used so this update should not be a large burden and can be explained in the deprecation message.

What is lost is the ability to test for unmodifiability via x is UnmodifiableUint8ListView. I have not found examples of this. If it is necessary to test for modifiability then an affordance should be provided that is not based on type tests.

How?

  • Replace the constructor for UnmodifiableUint8ListView with a new factory constructor Uint8List.unmodifiableView(Uint8List list), and likewise for the other unmodifiable views. The initial implementation of the factory constructors calls the existing private implementation classes.

  • Deprecate all the unmodifiable typed data view classes.

  • The JavaScript platforms can now change the unmodifiable variant to be a Uint8Array (possibly with a 'permission' tag, similar to how JavaScript's Array implements growable, fixed-length and constant Lists, or some other trick). This will be

  • Remove the deprecated classes.

  • It might be nice to add immutable: optional arguments to existing constructors. https://dart-review.googlesource.com/c/sdk/+/262241 is an attempt at this which is blocked because it makes the type of many Uint8Lists polymorphic. This will be less of an issue once the implementation type is monomorphic, although some compiler work will still be required to optimize writes.

When?

New factory constructors and deprecation should happen as soon as possible since it gates the other work.

Who?

@rakudrama

@rakudrama rakudrama added library-typed-data breaking-change-request This tracks requests for feedback on breaking changes labels Aug 15, 2023
@mkustermann mkustermann added the area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. label Aug 15, 2023
@mkustermann
Copy link
Member

TL;DR: To ensure that typed data has reliable performance on the JavaScript platforms we need to get rid of the UnmodifiableUint8ListView and similar classes.

Does dart2js have similar performance issues with Unmodifiable{List,Set,Map}View?

It might be nice to add immutable: optional arguments to existing constructors. https://dart-review.googlesource.com/c/sdk/+/262241 is an attempt at this which is blocked because it makes the type of many Uint8Lists polymorphic. This will be less of an issue once the implementation type is monomorphic, although some compiler work will still be required to optimize writes.

Very happy to hear that we may be able to move forward with cl/262241 😄

@lrhn
Copy link
Member

lrhn commented Aug 15, 2023

The argument for introducing UnmodifiableUint8ListView and the rest, were that some native platforms were presenting views of actually unmodifiable memory, and wanted to not cause the process to crash if it tried to write to unmodifiable memory.
Even if the view is only skin-deep, it was enough to protect users against themselves.

That use-case should still be supported, which means we need a way to create an unmodifiable view on an existing ByteBuffer, not just from a list of numbers.
For consistency, that means adding an asUnmodifiableUint8List(...) method on ByteBuffer, which is the way we create views on byte-buffers. Or adding an {bool unmodifiable = false} parameter to ByteBuffer.asUint8List - which we can't since it has optional positional parameters already.
(We can also add constructors like factory Uint8List.unmodifiableView(ByteBuffer buffer, [int? start, int? length]), but they should delegate to methods on ByteBuffer. Or I guess that design choice isn't as important any more, now that nobody else can implement ByteBuffer, so maybe we can get away with just having the constructors, and not expose how they work.)

Also, when you extract the buffer of an UnmodifiableUint8ListView (using .buffer) you get a special UnmodifiableByteBufferView which always creates unmodifaible views when you use its methods, like asUint8List.
We need to keep that property as well, even if we don't expose a separate UnmodifiableByteBufferView interface.
(Or deprecate and remove the methods on ByteBuffer, again now that nobody else can implement it, we don't have to provide a way for Uint8List.view(ByteBuffer,...) to work with user-created ByteBuffers.)

Optimially, that might just be one bit of information stored on the ByteBuffer, which makes its asUint8List create an unmodifiable view if the flag is set.
But if you can make an unmodifiable view on a modifiable buffer, then we actually do need to return a wrapper of that buffer from an unmodifiable Uint8List's .buffer, because we (probably) don't want to make the existing buffer unmodifiable by setting a bit on it.

That suggests another approach:

  • Every ByteBuffer is either modifiable or not, which is a fixed choice made at creation time.
  • Each view respects that (and can cache the information at view-creation time, since it's stable, perheaps even using a different implementation class for mutable and immutable views, if that's easier).
  • Creating a new unmodifiable Uint8List.unmodifiableFromList(source) will create a new unmodifiable buffer.
  • You cannot create an unmodifiable view on a modifiable buffer. (That's potentially breaking. Or rather, it removes a capability, but making the buffer modifiable isn't breking if it wasn't modified today, which it isn't since that would throw an error.)

Then the native use-case for an external-unmodifiable-memory backed ByteBuffer would just create an unmodifiable ByteBuffer first.

(The VM, which still has a non-view Uint8List, would have to have a way to mark those unmodifiable as well. They are effectively the underlying memory that the .buffer exposes.)

@rakudrama
Copy link
Member Author

TL;DR: To ensure that typed data has reliable performance on the JavaScript platforms we need to get rid of the UnmodifiableUint8ListView and similar classes.

Does dart2js have similar performance issues with Unmodifiable{List,Set,Map}View?

Yes for UnmodifiableListView, no for Set and Map.

For the receiver type List we need an interceptor-dispatch, where a[i] compiles to getInterceptor(a).$index(a, i). If the receiver is known to be an javaScript Array, this becomes JSArray_methods.$index(a, i) which is lowered it to a[boundsCheck(i, a.length)] and then we ignore the bounds check at -O4. If the receiver is known to be some class defined in Dart, getInterceptor(a) == a, so the access becomes a.$index(0, i) and possibly inlined.
But with both we have the two-level dispatch.

Less of a problem for Set and Map since both the collection and view are implemented as Dart objects, and often polymorphic (e.g. LinkedHashMap factory), so m[k] compiles to m.$index(0, k) which JavaScript may or may not detect is monomorphic.

It might be nice to add immutable: optional arguments to existing constructors. https://dart-review.googlesource.com/c/sdk/+/262241 is an attempt at this which is blocked because it makes the type of many Uint8Lists polymorphic. This will be less of an issue once the implementation type is monomorphic, although some compiler work will still be required to optimize writes.

Very happy to hear that we may be able to move forward with cl/262241 😄

@rakudrama
Copy link
Member Author

Optimally, that might just be one bit of information stored on the ByteBuffer, which makes its asUint8List create an unmodifiable view if the flag is set. But if you can make an unmodifiable view on a modifiable buffer, then we actually do need to return a wrapper of that buffer from an unmodifiable Uint8List's .buffer, because we (probably) don't want to make the existing buffer unmodifiable by setting a bit on it.

I am happy for now to make .buffer be polymorphic between the native ByteBuffer and an internal _UnmodifiableByteBufferView wrapper that carries the bit (or always use a wrapper, but we might not be able to do that).
Although the polymorphism problem is the same as for the view, it is incurred much less frequently - all you can really do with a buffer is create a view and then access the bytes, and that will be fast.

That suggests another approach:

  • Every ByteBuffer is either modifiable or not, which is a fixed choice made at creation time.
  • Each view respects that (and can cache the information at view-creation time, since it's stable, perhaps even using a different implementation class for mutable and immutable views, if that's easier).
  • Creating a new unmodifiable Uint8List.unmodifiableFromList(source) will create a new unmodifiable buffer.
  • You cannot create an unmodifiable view on a modifiable buffer. (That's potentially breaking. Or rather, it removes a capability, but making the buffer modifiable isn't breking if it wasn't modified today, which it isn't since that would throw an error.)

After protecting against SIGSEGV, this might be the most important use-case - handing off a view of data that was assembled via mutation. I'm not sure how, without additional magic, to implement Uint8List.fromLists(lists, immutable: true) with this constraint.

Then the native use-case for an external-unmodifiable-memory backed ByteBuffer would just create an unmodifiable ByteBuffer first.

(The VM, which still has a non-view Uint8List, would have to have a way to mark those unmodifiable as well. They are effectively the underlying memory that the .buffer exposes.)

@rakudrama
Copy link
Member Author

I'm thinking that adding an instance method might be better than another factory constructor.
The problem with a factory constructor called Uint8List.unmodifiableView(someUint8List) is that there already two factory constructors with 'view' in the name.

abstract final class Uint8List implements List<int>, _TypedIntList {
  external factory Uint8List(int length);
  external factory Uint8List.fromList(List<int> elements);

  /// Creates a [Uint8List] _view_ of the specified region in [buffer]....
  factory Uint8List.view(ByteBuffer buffer,
      [int offsetInBytes = 0, int? length]) ...

  /// Creates a [Uint8List] view on a range of elements of [data]....
  factory Uint8List.sublistView(TypedData data, [int start = 0, int? end]) { ... }

  /// Option 1: another factory constructor with 'view' in the name.
  external factory Uint8List.unmodifiableView(Uint8List list);

  /// Option2: an instance method.
  Uint8List asUnmodifiableView();

@itsjustkevin
Copy link
Contributor

@vsmenon @Hixie @grouma could you take a look at this breaking change request?

@grouma
Copy link
Member

grouma commented Sep 18, 2023

I can't find any usage in ACX.

@Hixie
Copy link
Contributor

Hixie commented Sep 22, 2023

fine by me. fyi @yjbanov

@lrhn
Copy link
Member

lrhn commented Sep 22, 2023

IIRC, the original request was from the assistant team. That's where we should look for existing uses.

@itsjustkevin
Copy link
Contributor

@vsmenon I am going to approve this change. Please remove the label if you see fit.

@itsjustkevin itsjustkevin added the cherry-pick-approved Label for approved cherrypick request label Oct 9, 2023
@vsmenon
Copy link
Member

vsmenon commented Oct 9, 2023

lgtm

@itsjustkevin itsjustkevin added breaking-change-approved and removed cherry-pick-approved Label for approved cherrypick request labels Oct 17, 2023
copybara-service bot pushed a commit that referenced this issue Oct 20, 2023
This is the first of several steps to remove the unmodifiable views for typed data classes. The end goal is that dart2js has only one class implementing `Uint8List` so that `Uint8List` accesses can always be compiled down to JavaScript code that directly uses indexed property accesses (`a[i]`).

This first step deprecates the unmodifiable view classes to help prevent their use in new code, and adds `asUnmodifiableView()` methods as a replacement for the small number of places that use the classes.

The next steps (see #53785) are to remove uses of the unmodifiable view classes from the SDK. Once this is complete the classes themselves can be removed.

TEST=ci

Issue: #53218
Issue: #53785

Change-Id: I04d4feb0d9f1619e6eee65236e559f5e6adf2661
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/321922
Reviewed-by: Nicholas Shahan <nshahan@google.com>
Reviewed-by: Lasse Nielsen <lrn@google.com>
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
Reviewed-by: Ömer Ağacan <omersa@google.com>
copybara-service bot pushed a commit that referenced this issue Oct 23, 2023
This reverts commit b657773.

Reason for revert: blocking Dart SDK -> Engine roll (flutter/flutter#137054)

Original change's description:
> [typed_data] Deprecate UnmodifiableUint8ListView and friends
>
> This is the first of several steps to remove the unmodifiable views for typed data classes. The end goal is that dart2js has only one class implementing `Uint8List` so that `Uint8List` accesses can always be compiled down to JavaScript code that directly uses indexed property accesses (`a[i]`).
>
> This first step deprecates the unmodifiable view classes to help prevent their use in new code, and adds `asUnmodifiableView()` methods as a replacement for the small number of places that use the classes.
>
> The next steps (see #53785) are to remove uses of the unmodifiable view classes from the SDK. Once this is complete the classes themselves can be removed.
>
> TEST=ci
>
> Issue: #53218
> Issue: #53785
>
> Change-Id: I04d4feb0d9f1619e6eee65236e559f5e6adf2661
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/321922
> Reviewed-by: Nicholas Shahan <nshahan@google.com>
> Reviewed-by: Lasse Nielsen <lrn@google.com>
> Commit-Queue: Stephen Adams <sra@google.com>
> Reviewed-by: Alexander Markov <alexmarkov@google.com>
> Reviewed-by: Martin Kustermann <kustermann@google.com>
> Reviewed-by: Ömer Ağacan <omersa@google.com>

Issue: #53218
Issue: #53785
Change-Id: I0bb042269f9ff8e5cd69619cf97b60c79ea98cbf
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/331680
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
Commit-Queue: Derek Xu <derekx@google.com>
@dcharkes
Copy link
Contributor

What is lost is the ability to test for unmodifiability via x is UnmodifiableUint8ListView. I have not found examples of this. If it is necessary to test for modifiability then an affordance should be provided that is not based on type tests.

We might have a use case: Marking object graphs deeply immutable to be able to share them across isolates in an isolate group.

For now, I'll just omit support for unmodifiable typed datas in my prototype.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. breaking-change-approved breaking-change-request This tracks requests for feedback on breaking changes library-typed-data
Projects
None yet
Development

No branches or pull requests

8 participants