diff --git a/packages/canvas/ImageAsset/index.ts b/packages/canvas/ImageAsset/index.ts index 1ff3dec2e..7b4979dc5 100644 --- a/packages/canvas/ImageAsset/index.ts +++ b/packages/canvas/ImageAsset/index.ts @@ -4,8 +4,6 @@ import { NAPI_HOST } from '../platform'; import { loadNativeImage } from '../platform/index'; let ctor; -// store ref if loading -const loaders = new Map(); export interface SvgLoadOptions { /** CSS pixels; one alone keeps the aspect ratio. Defaults to the SVG's size. */ @@ -91,6 +89,21 @@ export function fromSvgSource(value: T): T | ImageAsset { return svg ? (svgToImageAsset(svg) ?? value) : value; } +let nextAndroidLoad = 0; +const androidLoads = new Map void>(); + +function androidLoadCallback(complete: (success: boolean) => void) { + const id = nextAndroidLoad++; + androidLoads.set(id, complete); + return new (org).nativescript.canvas.NSCImageAsset.Callback({ + onComplete(success: boolean) { + const done = androidLoads.get(id); + androidLoads.delete(id); + done?.(success); + }, + }); +} + export class ImageAsset extends Observable { static { Helpers.initialize(); @@ -127,28 +140,22 @@ export class ImageAsset extends Observable { return this.native.fromUrlSync(url); } - private _incrementStrongRef() { - let count = 0; - if (loaders.has(this)) { - count = loaders.get(this); - } - count++; - loaders.set(this, count); - } - - private _decrementStrongRefAndRemove() { - // Parenthesised: `??` binds looser than `-`, so the old form returned the - // count undecremented and never released the strong ref. - const count = (loaders.get(this) ?? 0) - 1; - - if (count <= 0) { - loaders.delete(this); - } else { - loaders.set(this, count); - } + private _androidCallback(asset: number, resolve: (value: boolean) => void, reject: (reason?: any) => void, fallbackError?: string) { + return androidLoadCallback((success) => { + if (success) { + this.emitComplete(success, undefined); + resolve(success); + } else { + const error = (org).nativescript.canvas.NSCImageAsset.getError(asset) || fallbackError; + this.emitComplete(success, error); + reject(error); + } + }); } private emitComplete(success: boolean, error) { + // Not on the Node-API host. + this.native.__syncExternalMemory?.(); this.notify({ eventName: 'complete', object: this, complete: success, error }); } @@ -156,32 +163,10 @@ export class ImageAsset extends Observable { return new Promise((resolve, reject) => { if (__ANDROID__) { const asset = this._android.getAsset(); - const ref = new WeakRef(this); - (org).nativescript.canvas.NSCImageAsset.loadImageFromUrlAsync( - asset, - url, - new (org).nativescript.canvas.NSCImageAsset.Callback({ - onComplete(success: boolean) { - const owner = ref.get(); - if (!success) { - const error = (org).nativescript.canvas.NSCImageAsset.getError(asset); - if (owner) { - owner.emitComplete(success, error); - } - reject(error); - } else { - if (owner) { - owner.emitComplete(success, undefined); - } - resolve(success); - } - }, - }), - ); + (org).nativescript.canvas.NSCImageAsset.loadImageFromUrlAsync(asset, url, this._androidCallback(asset, resolve, reject)); return; } - this._incrementStrongRef(); this.native.fromUrlCb(url, (success, error) => { this.emitComplete(success, error); if (error) { @@ -189,7 +174,6 @@ export class ImageAsset extends Observable { } else { resolve(success); } - this._decrementStrongRefAndRemove(); }); }); } @@ -217,57 +201,14 @@ export class ImageAsset extends Observable { if (__ANDROID__) { const asset = this._android.getAsset(); - const ref = new WeakRef(this); if (typeof path === 'string' && path.indexOf('.webp') > -1) { - (org).nativescript.canvas.NSCImageAsset.loadWebPAsync( - asset, - path, - new (org).nativescript.canvas.NSCImageAsset.Callback({ - onComplete(success) { - const owner = ref.get(); - if (!success) { - const error = (org).nativescript.canvas.NSCImageAsset.getError(asset); - if (owner) { - owner.emitComplete(success, error); - } - reject(error); - } else { - if (owner) { - owner.emitComplete(success, undefined); - } - resolve(success); - } - }, - }), - ); + (org).nativescript.canvas.NSCImageAsset.loadWebPAsync(asset, path, this._androidCallback(asset, resolve, reject)); } else { - (org).nativescript.canvas.NSCImageAsset.loadImageFromPathAsync( - asset, - path, - new (org).nativescript.canvas.NSCImageAsset.Callback({ - onComplete(success) { - const owner = ref.get(); - if (!success) { - const error = (org).nativescript.canvas.NSCImageAsset.getError(asset); - if (owner) { - owner.emitComplete(success, error); - } - reject(error); - } else { - if (owner) { - owner.emitComplete(success, undefined); - } - resolve(success); - } - }, - }), - ); + (org).nativescript.canvas.NSCImageAsset.loadImageFromPathAsync(asset, path, this._androidCallback(asset, resolve, reject)); } return; } - this._incrementStrongRef(); - this.native.fromFileCb(path.toString(), (success, error) => { this.emitComplete(success, error); if (error) { @@ -275,8 +216,6 @@ export class ImageAsset extends Observable { } else { resolve(success); } - - this._decrementStrongRefAndRemove(); }); }); } @@ -315,30 +254,9 @@ export class ImageAsset extends Observable { return; } if (__ANDROID__) { - const ref = new WeakRef(this); const asset = long(this.native.__getRef()); try { - (org).nativescript.canvas.NSCImageAsset.loadImageFromBitmapAsync( - asset, - image, - new (org).nativescript.canvas.NSCImageAsset.Callback({ - onComplete(success) { - const owner = ref.get(); - if (!success) { - const error = (org).nativescript.canvas.NSCImageAsset.getError(asset) || 'Failed to load image from native source'; - if (owner) { - owner.emitComplete(success, error); - } - reject(error); - } else { - if (owner) { - owner.emitComplete(success, undefined); - } - resolve(success); - } - }, - }), - ); + (org).nativescript.canvas.NSCImageAsset.loadImageFromBitmapAsync(asset, image, this._androidCallback(asset, resolve, reject, 'Failed to load image from native source')); } catch (e) { reject(e); } @@ -346,23 +264,19 @@ export class ImageAsset extends Observable { } if (__IOS__) { - this._incrementStrongRef(); const asset = NSString.stringWithString(this.native.__getRef()); try { NSCImageAsset.loadImageFromImage(asset.longLongValue, image, (done) => { if (!done) { const error = this.error || 'Failed to load image from native source'; this.emitComplete(done, error); - this._decrementStrongRefAndRemove(); reject(error); } else { this.emitComplete(done, undefined); - this._decrementStrongRefAndRemove(); resolve(done); } }); } catch (e) { - this._decrementStrongRefAndRemove(); reject(e); } return; @@ -379,58 +293,16 @@ export class ImageAsset extends Observable { loadFromEncodedBytes(bytes: Uint8Array | Uint8ClampedArray) { return new Promise((resolve, reject) => { if (__ANDROID__) { - const ref = new WeakRef(this); const asset = this._android.getAsset(); if (!ArrayBuffer.isView(bytes)) { - (org).nativescript.canvas.NSCImageAsset.loadImageFromEncodedBytesAsync( - asset, - bytes, - new (org).nativescript.canvas.NSCImageAsset.Callback({ - onComplete(success) { - const owner = ref.get(); - if (!success) { - const error = (org).nativescript.canvas.NSCImageAsset.getError(asset); - if (owner) { - owner.emitComplete(success, error); - } - reject(error); - } else { - if (owner) { - owner.emitComplete(success, undefined); - } - resolve(success); - } - }, - }), - ); + (org).nativescript.canvas.NSCImageAsset.loadImageFromEncodedBytesAsync(asset, bytes, this._androidCallback(asset, resolve, reject)); } else { - (org).nativescript.canvas.NSCImageAsset.loadImageFromEncodedBufferAsync( - asset, - bytes, - new (org).nativescript.canvas.NSCImageAsset.Callback({ - onComplete(success) { - const owner = ref.get(); - if (!success) { - const error = (org).nativescript.canvas.NSCImageAsset.getError(asset); - if (owner) { - owner.emitComplete(success, error); - } - reject(error); - } else { - if (owner) { - owner.emitComplete(success, undefined); - } - resolve(success); - } - }, - }), - ); + (org).nativescript.canvas.NSCImageAsset.loadImageFromEncodedBufferAsync(asset, bytes, this._androidCallback(asset, resolve, reject)); } return; } - this._incrementStrongRef(); this.native.fromEncodedBytesCb(bytes, (success, error) => { this.emitComplete(success, error); if (error) { @@ -438,8 +310,6 @@ export class ImageAsset extends Observable { } else { resolve(success); } - - this._decrementStrongRefAndRemove(); }); }); } @@ -462,52 +332,25 @@ export class ImageAsset extends Observable { } catch (error) { return Promise.reject(error); } - this._incrementStrongRef(); - return provider - .load(source, this, options) - .then( - (success) => { - this.emitComplete(success, success ? undefined : this.error); - return success; - }, - (error) => { - this.emitComplete(false, error); - throw error; - }, - ) - .finally(() => this._decrementStrongRefAndRemove()); + return provider.load(source, this, options).then( + (success) => { + this.emitComplete(success, success ? undefined : this.error); + return success; + }, + (error) => { + this.emitComplete(false, error); + throw error; + }, + ); } loadFromBytes(width: number, height: number, bytes: Uint8Array | Uint8ClampedArray) { return new Promise((resolve, reject) => { if (__ANDROID__) { - const ref = new WeakRef(this); const asset = this._android.getAsset(); if (Array.isArray(bytes)) { - (org).nativescript.canvas.NSCImageAsset.loadImageFromBytesAsync( - asset, - width, - height, - bytes, - new (org).nativescript.canvas.NSCImageAsset.Callback({ - onComplete(success) { - const owner = ref.get(); - if (!success) { - const error = (org).nativescript.canvas.NSCImageAsset.getError(asset); - if (owner) { - owner.emitComplete(success, error); - } - reject(error); - } else { - if (owner) { - owner.emitComplete(success, undefined); - } - resolve(success); - } - }, - }), - ); + (org).nativescript.canvas.NSCImageAsset.loadImageFromBytesAsync(asset, width, height, bytes, this._androidCallback(asset, resolve, reject)); } else { let buffer: ArrayBuffer = bytes as never; if (ArrayBuffer.isView(bytes)) { @@ -516,34 +359,11 @@ export class ImageAsset extends Observable { buffer = buffer.nativeObject as never; } - (org).nativescript.canvas.NSCImageAsset.loadImageFromBufferAsync( - asset, - width, - height, - buffer, - new (org).nativescript.canvas.NSCImageAsset.Callback({ - onComplete(success) { - const owner = ref.get(); - if (!success) { - const error = (org).nativescript.canvas.NSCImageAsset.getError(asset); - if (owner) { - owner.emitComplete(success, error); - } - reject(error); - } else { - if (owner) { - owner.emitComplete(success, undefined); - } - resolve(success); - } - }, - }), - ); + (org).nativescript.canvas.NSCImageAsset.loadImageFromBufferAsync(asset, width, height, buffer, this._androidCallback(asset, resolve, reject)); } return; } - this._incrementStrongRef(); this.native.fromBytesCb(width, height, bytes, (success, error) => { this.emitComplete(success, error); if (error) { @@ -551,8 +371,6 @@ export class ImageAsset extends Observable { } else { resolve(success); } - - this._decrementStrongRefAndRemove(); }); }); } @@ -567,16 +385,12 @@ export class ImageAsset extends Observable { save(path: string, format: ImageAssetSaveFormat): Promise { return new Promise((resolve, reject) => { - this._incrementStrongRef(); - this.native.saveCb(path, format, (success, error) => { if (error) { reject(error); } else { resolve(success); } - - this._decrementStrongRefAndRemove(); }); }); } diff --git a/packages/canvas/platforms/ios/src/cpp/AsyncCallback.h b/packages/canvas/platforms/ios/src/cpp/AsyncCallback.h index 76cc530fd..e4eb75e53 100644 --- a/packages/canvas/platforms/ios/src/cpp/AsyncCallback.h +++ b/packages/canvas/platforms/ios/src/cpp/AsyncCallback.h @@ -132,6 +132,7 @@ struct AsyncCallback { if (isPrepared_) { ALooper_removeFd(looper_, fd_[0]); close(fd_[0]); + close(fd_[1]); fd_[0] = -1; fd_[1] = -1; ALooper_release(looper_); @@ -160,6 +161,7 @@ struct AsyncCallback { } ALooper_removeFd(looper_, fd_[0]); close(fd_[0]); + close(fd_[1]); ALooper_release(looper_); callback_.Reset(); } diff --git a/packages/canvas/platforms/ios/src/cpp/ImageAssetImpl.cpp b/packages/canvas/platforms/ios/src/cpp/ImageAssetImpl.cpp index c9d659fa4..2affa607a 100644 --- a/packages/canvas/platforms/ios/src/cpp/ImageAssetImpl.cpp +++ b/packages/canvas/platforms/ios/src/cpp/ImageAssetImpl.cpp @@ -70,6 +70,10 @@ v8::Local ImageAssetImpl::GetCtor(v8::Isolate *isolate) { tmpl->Set( ConvertToV8String(isolate, "__getRef"), v8::FunctionTemplate::New(isolate, GetReference)); + + tmpl->Set( + ConvertToV8String(isolate, "__syncExternalMemory"), + v8::FunctionTemplate::New(isolate, SyncExternalMemory)); @@ -199,6 +203,20 @@ ImageAssetImpl::GetReference(const v8::FunctionCallbackInfo &args) { args.GetReturnValue().SetEmptyString(); } +void ImageAssetImpl::UpdateExternalMemory(v8::Isolate *isolate) { + auto asset = this->GetImageAsset(); + auto bytes = (int64_t) canvas_native_image_asset_width(asset) * + (int64_t) canvas_native_image_asset_height(asset) * 4; + SetExternalMemory(isolate, bytes); +} + +void ImageAssetImpl::SyncExternalMemory(const v8::FunctionCallbackInfo &args) { + auto ptr = GetPointer(args.This()); + if (ptr != nullptr) { + ptr->UpdateExternalMemory(args.GetIsolate()); + } +} + void ImageAssetImpl::GetError(v8::Local name, const v8::PropertyCallbackInfo &info) { @@ -223,6 +241,7 @@ void ImageAssetImpl::FromUrlSync(const v8::FunctionCallbackInfo &args auto url = ConvertFromV8String(isolate, args[0]); auto done = canvas_native_image_asset_load_from_url(ptr->GetImageAsset(), url.c_str()); + ptr->UpdateExternalMemory(isolate); args.GetReturnValue().Set(done); } @@ -393,6 +412,7 @@ void ImageAssetImpl::FromFileSync(const v8::FunctionCallbackInfo &arg auto path = ConvertFromV8String(isolate, args[0]); auto done = canvas_native_image_asset_load_from_path(ptr->GetImageAsset(), path.c_str()); + ptr->UpdateExternalMemory(isolate); args.GetReturnValue().Set(done); } @@ -526,6 +546,7 @@ void ImageAssetImpl::FromBytesSync(const v8::FunctionCallbackInfo &ar done = canvas_native_image_asset_load_from_raw(ptr->GetImageAsset(), width, height, bytes.data, bytes.size); } } + ptr->UpdateExternalMemory(args.GetIsolate()); args.GetReturnValue().Set(done); return; @@ -664,6 +685,7 @@ void ImageAssetImpl::FromEncodedBytesSync(const v8::FunctionCallbackInfoGetImageAsset(), bytes.data, bytes.size); + ptr->UpdateExternalMemory(args.GetIsolate()); args.GetReturnValue().Set(done); return; diff --git a/packages/canvas/platforms/ios/src/cpp/ImageAssetImpl.h b/packages/canvas/platforms/ios/src/cpp/ImageAssetImpl.h index ebce3a5a6..a80a49104 100644 --- a/packages/canvas/platforms/ios/src/cpp/ImageAssetImpl.h +++ b/packages/canvas/platforms/ios/src/cpp/ImageAssetImpl.h @@ -44,6 +44,10 @@ class ImageAssetImpl : ObjectWrapperImpl { static void GetReference(const v8::FunctionCallbackInfo &args); + static void SyncExternalMemory(const v8::FunctionCallbackInfo &args); + + void UpdateExternalMemory(v8::Isolate *isolate); + static void GetError(v8::Local name, const v8::PropertyCallbackInfo &info); diff --git a/packages/canvas/platforms/ios/src/cpp/ImageBitmapImpl.h b/packages/canvas/platforms/ios/src/cpp/ImageBitmapImpl.h index ee66cf420..e757a9f65 100644 --- a/packages/canvas/platforms/ios/src/cpp/ImageBitmapImpl.h +++ b/packages/canvas/platforms/ios/src/cpp/ImageBitmapImpl.h @@ -41,6 +41,10 @@ class ImageBitmapImpl : public ObjectWrapperImpl { impl->BindFinalizer(isolate, object); + auto bitmap = static_cast(impl)->GetImageAsset(); + impl->SetExternalMemory(isolate, (int64_t) canvas_native_image_asset_width(bitmap) * + (int64_t) canvas_native_image_asset_height(bitmap) * 4); + return scope.Escape(object); } diff --git a/packages/canvas/platforms/ios/src/cpp/JSICallback.h b/packages/canvas/platforms/ios/src/cpp/JSICallback.h index 95ca3c689..424ada905 100644 --- a/packages/canvas/platforms/ios/src/cpp/JSICallback.h +++ b/packages/canvas/platforms/ios/src/cpp/JSICallback.h @@ -77,6 +77,7 @@ struct JSICallback { ~JSICallback() { ALooper_removeFd(looper_, fd_[0]); close(fd_[0]); + close(fd_[1]); ALooper_release(looper_); auto callback = callback_.get(); if(callback != nullptr){ diff --git a/packages/canvas/platforms/ios/src/cpp/JSIReadFileCallback.h b/packages/canvas/platforms/ios/src/cpp/JSIReadFileCallback.h index 5847797bd..008e11552 100644 --- a/packages/canvas/platforms/ios/src/cpp/JSIReadFileCallback.h +++ b/packages/canvas/platforms/ios/src/cpp/JSIReadFileCallback.h @@ -84,6 +84,7 @@ struct JSIReadFileCallback { ~JSIReadFileCallback() { ALooper_removeFd(looper_, fd_[0]); close(fd_[0]); + close(fd_[1]); ALooper_release(looper_); callback_.Reset(); canvas_native_u8_buffer_release(data_); diff --git a/packages/canvas/platforms/ios/src/cpp/ObjectWrapperImpl.h b/packages/canvas/platforms/ios/src/cpp/ObjectWrapperImpl.h index 5d95ae34e..918f86709 100644 --- a/packages/canvas/platforms/ios/src/cpp/ObjectWrapperImpl.h +++ b/packages/canvas/platforms/ios/src/cpp/ObjectWrapperImpl.h @@ -10,6 +10,7 @@ #include "Common.h" #include "NativeType.h" +#include "include/v8-external-memory-accounter.h" class ObjectWrapperImpl { public: @@ -26,9 +27,20 @@ class ObjectWrapperImpl { static void Finalizer(const v8::WeakCallbackInfo &data) { auto *pThis = data.GetParameter(); pThis->weakHandle_.Reset(); + pThis->SetExternalMemory(data.GetIsolate(), 0); delete pThis; } + // Native bytes V8 should count against this wrapper. + void SetExternalMemory(v8::Isolate *isolate, int64_t bytes) { + auto delta = bytes - externalBytes_; + if (delta == 0) { + return; + } + externalMemory_.Update(isolate, delta); + externalBytes_ = bytes; + } + void BindFinalizer(v8::Isolate *isolate, const v8::Local &object) { v8::HandleScope scopedHandle(isolate); weakHandle_.Reset(isolate, object); @@ -85,6 +97,8 @@ class ObjectWrapperImpl { private: v8::Global weakHandle_; + v8::ExternalMemoryAccounter externalMemory_; + int64_t externalBytes_ = 0; }; diff --git a/packages/canvas/platforms/ios/src/cpp/PromiseCallback.h b/packages/canvas/platforms/ios/src/cpp/PromiseCallback.h index 5324e4416..f920b2bd1 100644 --- a/packages/canvas/platforms/ios/src/cpp/PromiseCallback.h +++ b/packages/canvas/platforms/ios/src/cpp/PromiseCallback.h @@ -137,6 +137,7 @@ struct PromiseCallback { if (isPrepared_) { ALooper_removeFd(looper_, fd_[0]); close(fd_[0]); + close(fd_[1]); fd_[0] = -1; fd_[1] = -1; ALooper_release(looper_); @@ -178,6 +179,7 @@ struct PromiseCallback { } ALooper_removeFd(looper_, fd_[0]); close(fd_[0]); + close(fd_[1]); ALooper_release(looper_); callback_.Reset(); }