Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
5 changes: 5 additions & 0 deletions lib/ui/painting/canvas.cc
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ void Canvas::drawImage(const CanvasImage* image,
if (!image)
Dart_ThrowException(
ToDart("Canvas.drawImage called with non-genuine Image."));
image_allocation_size_ += image->GetAllocationSize();
canvas_->drawImage(image->image(), x, y, paint.paint());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this accurate? I would have expected the canvas to rasterize the drawn image at some point so it doesn't actually hold on to the full image.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's going to hold a reference to the SkImage as long as its alive. The memory is not necessarily duplicated (e.g. we don't necessarily have 2x the pixel allocation for the extra reference), but we need to do a better job of letting the GC know how expensive the object is, otherwise it will think it can skip a GC pass when it really needs one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might help to clarify the semantics of "allocation size". The allocation size is not the size of the allocation that will be collected when the owning object is collected. Instead, it is the size of the allocation that is guaranteed to stay alive pending the collection of the owning object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in a doc comment in dart_wrappable.h?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I don't know of a better place.

}

Expand All @@ -314,6 +315,7 @@ void Canvas::drawImageRect(const CanvasImage* image,
ToDart("Canvas.drawImageRect called with non-genuine Image."));
SkRect src = SkRect::MakeLTRB(src_left, src_top, src_right, src_bottom);
SkRect dst = SkRect::MakeLTRB(dst_left, dst_top, dst_right, dst_bottom);
image_allocation_size_ += image->GetAllocationSize();
canvas_->drawImageRect(image->image(), src, dst, paint.paint(),
SkCanvas::kFast_SrcRectConstraint);
}
Expand All @@ -339,6 +341,7 @@ void Canvas::drawImageNine(const CanvasImage* image,
SkIRect icenter;
center.round(&icenter);
SkRect dst = SkRect::MakeLTRB(dst_left, dst_top, dst_right, dst_bottom);
image_allocation_size_ += image->GetAllocationSize();
canvas_->drawImageNine(image->image(), icenter, dst, paint.paint());
}

Expand All @@ -348,6 +351,7 @@ void Canvas::drawPicture(Picture* picture) {
if (!picture)
Dart_ThrowException(
ToDart("Canvas.drawPicture called with non-genuine Picture."));
image_allocation_size_ += picture->GetAllocationSize();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You missed a few:

  • drawAtlas references an image.
  • drawVertices and drawPoints has vertex buffers whose sizes can be appreciable (IIRC stuff like flare uses it extensively).
  • drawPath can have custom paths that may be large. There are path iterator APIs that you can use to walk an count the verbs and points. You should probably memoize the size here too.
  • All arguments that can take a paint may have a shader set on them. These shaders can reference images. In the same vein, shaders in dart:ui should also reference the correct size.

I understand if you don't want to audit the existing codebase and track everything in this one patch. Let's file a bug to follow up on to perform correct book-keeping on the resources referenced by dart:ui call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm including drawAtlas in this patch. Filed flutter/flutter#58438 for the rest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed flutter/flutter#58440 specifically for ImageShader, since that's a little different.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Thanks.

canvas_->drawPicture(picture->picture().get());
}

Expand Down Expand Up @@ -402,6 +406,7 @@ void Canvas::drawAtlas(const Paint& paint,
static_assert(sizeof(SkRect) == sizeof(float) * 4,
"SkRect doesn't use floats.");

image_allocation_size_ += atlas->GetAllocationSize();
canvas_->drawAtlas(
skImage.get(), reinterpret_cast<const SkRSXform*>(transforms.data()),
reinterpret_cast<const SkRect*>(rects.data()),
Expand Down
3 changes: 3 additions & 0 deletions lib/ui/painting/canvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,16 @@ class Canvas : public RefCountedDartWrappable<Canvas> {

static void RegisterNatives(tonic::DartLibraryNatives* natives);

size_t image_allocation_size() const { return image_allocation_size_; }

private:
explicit Canvas(SkCanvas* canvas);

// The SkCanvas is supplied by a call to SkPictureRecorder::beginRecording,
// which does not transfer ownership. For this reason, we hold a raw
// pointer and manually set to null in Clear.
SkCanvas* canvas_;
size_t image_allocation_size_ = 0;
};

} // namespace flutter
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/painting/engine_layer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ EngineLayer::EngineLayer(std::shared_ptr<flutter::ContainerLayer> layer)

EngineLayer::~EngineLayer() = default;

size_t EngineLayer::GetAllocationSize() {
size_t EngineLayer::GetAllocationSize() const {
// Provide an approximation of the total memory impact of this object to the
// Dart GC. The ContainerLayer may hold references to a tree of other layers,
// which in turn may contain Skia objects.
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/painting/engine_layer.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class EngineLayer : public RefCountedDartWrappable<EngineLayer> {
public:
~EngineLayer() override;

size_t GetAllocationSize() override;
size_t GetAllocationSize() const override;

static fml::RefPtr<EngineLayer> MakeRetained(
std::shared_ptr<flutter::ContainerLayer> layer) {
Expand Down
3 changes: 2 additions & 1 deletion lib/ui/painting/image.cc
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ Dart_Handle CanvasImage::toByteData(int format, Dart_Handle callback) {

void CanvasImage::dispose() {
ClearDartWrapper();
image_.reset();
}

size_t CanvasImage::GetAllocationSize() {
size_t CanvasImage::GetAllocationSize() const {
if (auto image = image_.get()) {
const auto& info = image->imageInfo();
const auto kMipmapOverhead = 4.0 / 3.0;
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/painting/image.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class CanvasImage final : public RefCountedDartWrappable<CanvasImage> {
image_ = std::move(image);
}

size_t GetAllocationSize() override;
size_t GetAllocationSize() const override;

static void RegisterNatives(tonic::DartLibraryNatives* natives);

Expand Down
4 changes: 2 additions & 2 deletions lib/ui/painting/image_decoder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ static SkiaGPUObject<SkImage> UploadRasterImage(
SkSafeUnref(static_cast<SkImage*>(context));
},
image.get());
result = {texture_image, nullptr};
result = {std::move(texture_image), nullptr};
})
.SetIfFalse([&result, context = io_manager->GetResourceContext(),
&pixmap, queue = io_manager->GetSkiaUnrefQueue()] {
Expand All @@ -293,7 +293,7 @@ static SkiaGPUObject<SkImage> UploadRasterImage(
FML_LOG(ERROR) << "Could not make x-context image.";
result = {};
} else {
result = {texture_image, queue};
result = {std::move(texture_image), queue};
}
}));

Expand Down
21 changes: 13 additions & 8 deletions lib/ui/painting/picture.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,20 @@ IMPLEMENT_WRAPPERTYPEINFO(ui, Picture);

DART_BIND_ALL(Picture, FOR_EACH_BINDING)

fml::RefPtr<Picture> Picture::Create(
Dart_Handle dart_handle,
flutter::SkiaGPUObject<SkPicture> picture) {
auto canvas_picture = fml::MakeRefCounted<Picture>(std::move(picture));
fml::RefPtr<Picture> Picture::Create(Dart_Handle dart_handle,
flutter::SkiaGPUObject<SkPicture> picture,
size_t image_allocation_size) {
auto canvas_picture =
fml::MakeRefCounted<Picture>(std::move(picture), image_allocation_size);

canvas_picture->AssociateWithDartWrapper(dart_handle);
return canvas_picture;
}

Picture::Picture(flutter::SkiaGPUObject<SkPicture> picture)
: picture_(std::move(picture)) {}
Picture::Picture(flutter::SkiaGPUObject<SkPicture> picture,
size_t image_allocation_size)
: picture_(std::move(picture)),
image_allocation_size_(image_allocation_size) {}

Picture::~Picture() = default;

Expand All @@ -52,11 +55,13 @@ Dart_Handle Picture::toImage(uint32_t width,

void Picture::dispose() {
ClearDartWrapper();
picture_.reset();
}

size_t Picture::GetAllocationSize() {
size_t Picture::GetAllocationSize() const {
if (auto picture = picture_.get()) {
return picture->approximateBytesUsed();
return picture->approximateBytesUsed() + sizeof(Picture) +
image_allocation_size_;
} else {
return sizeof(Picture);
}
Expand Down
12 changes: 9 additions & 3 deletions lib/ui/painting/picture.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "flutter/flow/skia_gpu_object.h"
#include "flutter/lib/ui/dart_wrapper.h"
#include "flutter/lib/ui/painting/image.h"
#include "flutter/lib/ui/ui_dart_state.h"
#include "third_party/skia/include/core/SkPicture.h"

namespace tonic {
Expand All @@ -24,7 +25,8 @@ class Picture : public RefCountedDartWrappable<Picture> {
public:
~Picture() override;
static fml::RefPtr<Picture> Create(Dart_Handle dart_handle,
flutter::SkiaGPUObject<SkPicture> picture);
flutter::SkiaGPUObject<SkPicture> picture,
size_t image_allocation_size);

sk_sp<SkPicture> picture() const { return picture_.get(); }

Expand All @@ -34,7 +36,7 @@ class Picture : public RefCountedDartWrappable<Picture> {

void dispose();

size_t GetAllocationSize() override;
size_t GetAllocationSize() const override;

static void RegisterNatives(tonic::DartLibraryNatives* natives);

Expand All @@ -43,10 +45,14 @@ class Picture : public RefCountedDartWrappable<Picture> {
uint32_t height,
Dart_Handle raw_image_callback);

size_t image_allocation_size() const { return image_allocation_size_; }

private:
explicit Picture(flutter::SkiaGPUObject<SkPicture> picture);
Picture(flutter::SkiaGPUObject<SkPicture> picture,
size_t image_allocation_size_);

flutter::SkiaGPUObject<SkPicture> picture_;
size_t image_allocation_size_;
};

} // namespace flutter
Expand Down
9 changes: 6 additions & 3 deletions lib/ui/painting/picture_recorder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ fml::RefPtr<Picture> PictureRecorder::endRecording(Dart_Handle dart_picture) {
if (!isRecording())
return nullptr;

fml::RefPtr<Picture> picture = Picture::Create(
dart_picture, UIDartState::CreateGPUObject(
picture_recorder_.finishRecordingAsPicture()));
fml::RefPtr<Picture> picture =
Picture::Create(dart_picture,
UIDartState::CreateGPUObject(
picture_recorder_.finishRecordingAsPicture()),
canvas_->image_allocation_size());

canvas_->Clear();
canvas_->ClearDartWrapper();
canvas_ = nullptr;
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/painting/single_frame_codec.cc
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Dart_Handle SingleFrameCodec::getNextFrame(Dart_Handle callback_handle) {
return Dart_Null();
}

size_t SingleFrameCodec::GetAllocationSize() {
size_t SingleFrameCodec::GetAllocationSize() const {
const auto& data = descriptor_.data;
const auto data_byte_size = data ? data->size() : 0;
const auto frame_byte_size = (cached_frame_ && cached_frame_->image())
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/painting/single_frame_codec.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class SingleFrameCodec : public Codec {
Dart_Handle getNextFrame(Dart_Handle args) override;

// |DartWrappable|
size_t GetAllocationSize() override;
size_t GetAllocationSize() const override;

private:
enum class Status { kNew, kInProgress, kComplete };
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/text/paragraph.cc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Paragraph::Paragraph(std::unique_ptr<txt::Paragraph> paragraph)

Paragraph::~Paragraph() = default;

size_t Paragraph::GetAllocationSize() {
size_t Paragraph::GetAllocationSize() const {
// We don't have an accurate accounting of the paragraph's memory consumption,
// so return a fixed size to indicate that its impact is more than the size
// of the Paragraph class.
Expand Down
2 changes: 1 addition & 1 deletion lib/ui/text/paragraph.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class Paragraph : public RefCountedDartWrappable<Paragraph> {
Dart_Handle getLineBoundary(unsigned offset);
tonic::Float64List computeLineMetrics();

size_t GetAllocationSize() override;
size_t GetAllocationSize() const override;

static void RegisterNatives(tonic::DartLibraryNatives* natives);

Expand Down
47 changes: 47 additions & 0 deletions testing/dart/canvas_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

// @dart = 2.6
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';
Expand All @@ -13,6 +14,24 @@ import 'package:test/test.dart';

typedef CanvasCallback = void Function(Canvas canvas);

Future<Image> createImage(int width, int height) {
final Completer<Image> completer = Completer<Image>();
decodeImageFromPixels(
Uint8List.fromList(List<int>.generate(
width * height * 4,
(int pixel) => pixel % 255,
)),
width,
height,
PixelFormat.rgba8888,
(Image image) {
completer.complete(image);
},
);

return completer.future;
}

void testCanvas(CanvasCallback callback) {
try {
callback(Canvas(PictureRecorder(), const Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)));
Expand Down Expand Up @@ -198,4 +217,32 @@ void main() {
await fuzzyGoldenImageCompare(image, 'canvas_test_dithered_gradient.png');
expect(areEqual, true);
}, skip: !Platform.isLinux); // https://github.com/flutter/flutter/issues/53784

test('Image size reflected in picture size for image*, drawAtlas, and drawPicture methods', () async {
final Image image = await createImage(100, 100);
final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
const Rect rect = Rect.fromLTWH(0, 0, 100, 100);
canvas.drawImage(image, Offset.zero, Paint());
canvas.drawImageRect(image, rect, rect, Paint());
canvas.drawImageNine(image, rect, rect, Paint());
canvas.drawAtlas(image, <RSTransform>[], <Rect>[], <Color>[], BlendMode.src, rect, Paint());
final Picture picture = recorder.endRecording();

// Some of the numbers here appear to utilize sharing/reuse of common items,
// e.g. of the Paint() or same `Rect` usage, etc.
// The raw utilization of a 100x100 picture here should be 53333:
// 100 * 100 * 4 * (4/3) = 53333.333333....
// To avoid platform specific idiosyncrasies and brittleness against changes
// to Skia, we just assert this is _at least_ 4x the image size.
const int minimumExpected = 53333 * 4;
expect(picture.approximateBytesUsed, greaterThan(minimumExpected));

final PictureRecorder recorder2 = PictureRecorder();
final Canvas canvas2 = Canvas(recorder2);
canvas2.drawPicture(picture);
final Picture picture2 = recorder2.endRecording();

expect(picture2.approximateBytesUsed, greaterThan(minimumExpected));
});
}
2 changes: 1 addition & 1 deletion third_party/tonic/dart_wrappable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ void DartWrappable::FinalizeDartWrapper(void* isolate_callback_data,
wrappable->ReleaseDartWrappableReference(); // Balanced in CreateDartWrapper.
}

size_t DartWrappable::GetAllocationSize() {
size_t DartWrappable::GetAllocationSize() const {
return GetDartWrapperInfo().size_in_bytes;
}

Expand Down
2 changes: 1 addition & 1 deletion third_party/tonic/dart_wrappable.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class DartWrappable {
// Override this to customize the object size reported to the Dart garbage
// collector.
// Implement using IMPLEMENT_WRAPPERTYPEINFO macro
virtual size_t GetAllocationSize();
virtual size_t GetAllocationSize() const;

virtual void RetainDartWrappableReference() const = 0;

Expand Down