mirror of
https://github.com/dart-lang/sdk
synced 2024-10-14 16:55:08 +00:00
Reland "[vm, compiler] Prune dictionaries to only contain elements needed at runtime."
This is a reland of commit 6de162e014
Original change's description:
> [vm, compiler] Prune dictionaries to only contain elements needed at runtime.
>
> flutter_gallery
> Isolate(CodeSize): 2116400 -> 1981238 (-6.28%)
> Total(CodeSize): 7217938 -> 7082600 (-1.87%)
>
> TEST=ci
> Bug: https://github.com/dart-lang/sdk/issues/48910
> Change-Id: I8cd285ddab3a611cd7a2a91d50414be402f8543a
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/244303
> Reviewed-by: Siva Annamalai <asiva@google.com>
> Commit-Queue: Ryan Macnak <rmacnak@google.com>
TEST=ci
Bug: https://github.com/dart-lang/sdk/issues/48910
Change-Id: I3d3ecd04369585547963fb3efff1b3ff0723d8f8
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/246990
Commit-Queue: Ryan Macnak <rmacnak@google.com>
Reviewed-by: Siva Annamalai <asiva@google.com>
This commit is contained in:
parent
3d1c991fcf
commit
4c82eb5836
|
@ -1900,12 +1900,12 @@ class CodeSerializationCluster : public SerializationCluster {
|
|||
// in the current loading unit).
|
||||
ObjectPoolPtr pool = code->untag()->object_pool_;
|
||||
if (s->kind() == Snapshot::kFullAOT) {
|
||||
TracePool(s, pool, /*only_code=*/is_deferred);
|
||||
TracePool(s, pool, /*only_call_targets=*/is_deferred);
|
||||
} else {
|
||||
if (s->InCurrentLoadingUnitOrRoot(pool)) {
|
||||
s->Push(pool);
|
||||
} else {
|
||||
TracePool(s, pool, /*only_code=*/true);
|
||||
TracePool(s, pool, /*only_call_targets=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1973,7 +1973,7 @@ class CodeSerializationCluster : public SerializationCluster {
|
|||
#endif
|
||||
}
|
||||
|
||||
void TracePool(Serializer* s, ObjectPoolPtr pool, bool only_code) {
|
||||
void TracePool(Serializer* s, ObjectPoolPtr pool, bool only_call_targets) {
|
||||
if (pool == ObjectPool::null()) {
|
||||
return;
|
||||
}
|
||||
|
@ -1984,8 +1984,18 @@ class CodeSerializationCluster : public SerializationCluster {
|
|||
auto entry_type = ObjectPool::TypeBits::decode(entry_bits[i]);
|
||||
if (entry_type == ObjectPool::EntryType::kTaggedObject) {
|
||||
const ObjectPtr target = pool->untag()->data()[i].raw_obj_;
|
||||
if (!only_code || target->IsCode()) {
|
||||
// A field is a call target because its initializer may be called
|
||||
// indirectly by passing the field to the runtime. A const closure
|
||||
// is a call target because its function may be called indirectly
|
||||
// via a closure call.
|
||||
if (!only_call_targets || target->IsCode() || target->IsFunction() ||
|
||||
target->IsField() || target->IsClosure()) {
|
||||
s->Push(target);
|
||||
} else {
|
||||
intptr_t cid = target->GetClassIdMayBeSmi();
|
||||
if (cid >= kNumPredefinedCids) {
|
||||
s->Push(s->isolate_group()->class_table()->At(cid));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -424,6 +424,7 @@ Precompiler::Precompiler(Thread* thread)
|
|||
typeparams_to_retain_(),
|
||||
consts_to_retain_(),
|
||||
seen_table_selectors_(),
|
||||
api_uses_(),
|
||||
error_(Error::Handle()),
|
||||
get_runtime_type_is_unique_(false) {
|
||||
ASSERT(Precompiler::singleton_ == NULL);
|
||||
|
@ -662,6 +663,8 @@ void Precompiler::DoCompileAll() {
|
|||
ProgramVisitor::Dedup(T);
|
||||
}
|
||||
|
||||
PruneDictionaries();
|
||||
|
||||
if (retained_reasons_writer_ != nullptr) {
|
||||
reasons_writer.Write();
|
||||
retained_reasons_writer_ = nullptr;
|
||||
|
@ -752,6 +755,7 @@ void Precompiler::AddRoots() {
|
|||
}
|
||||
}
|
||||
if (!main.IsNull()) {
|
||||
AddApiUse(main);
|
||||
if (lib.LookupLocalFunction(name) == Function::null()) {
|
||||
retain_root_library_caches_ = true;
|
||||
}
|
||||
|
@ -1464,6 +1468,14 @@ bool Precompiler::IsHitByTableSelector(const Function& function) {
|
|||
return seen_table_selectors_.HasKey(selector_id);
|
||||
}
|
||||
|
||||
void Precompiler::AddApiUse(const Object& obj) {
|
||||
api_uses_.Insert(&Object::ZoneHandle(Z, obj.ptr()));
|
||||
}
|
||||
|
||||
bool Precompiler::HasApiUse(const Object& obj) {
|
||||
return api_uses_.HasKey(&obj);
|
||||
}
|
||||
|
||||
void Precompiler::AddInstantiatedClass(const Class& cls) {
|
||||
if (is_tracing()) {
|
||||
tracer_->WriteClassInstantiationRef(cls);
|
||||
|
@ -1523,6 +1535,7 @@ void Precompiler::AddAnnotatedRoots() {
|
|||
&reusable_object_handle) ==
|
||||
EntryPointPragma::kAlways) {
|
||||
AddInstantiatedClass(cls);
|
||||
AddApiUse(cls);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1541,6 +1554,7 @@ void Precompiler::AddAnnotatedRoots() {
|
|||
if (pragma == EntryPointPragma::kNever) continue;
|
||||
|
||||
AddField(field);
|
||||
AddApiUse(field);
|
||||
|
||||
if (!field.is_static()) {
|
||||
if (pragma != EntryPointPragma::kSetterOnly) {
|
||||
|
@ -1569,6 +1583,7 @@ void Precompiler::AddAnnotatedRoots() {
|
|||
type == EntryPointPragma::kCallOnly) {
|
||||
functions_with_entry_point_pragmas_.Insert(function);
|
||||
AddFunction(function, RetainReasons::kEntryPointPragma);
|
||||
AddApiUse(function);
|
||||
}
|
||||
|
||||
if ((type == EntryPointPragma::kAlways ||
|
||||
|
@ -1578,10 +1593,16 @@ void Precompiler::AddAnnotatedRoots() {
|
|||
function2 = function.ImplicitClosureFunction();
|
||||
functions_with_entry_point_pragmas_.Insert(function2);
|
||||
AddFunction(function2, RetainReasons::kEntryPointPragma);
|
||||
|
||||
// Not `function2`: Dart_GetField will lookup the regular function
|
||||
// and get the implicit closure function from that.
|
||||
AddApiUse(function);
|
||||
}
|
||||
|
||||
if (function.IsGenerativeConstructor()) {
|
||||
AddInstantiatedClass(cls);
|
||||
AddApiUse(function);
|
||||
AddApiUse(cls);
|
||||
}
|
||||
}
|
||||
if (function.kind() == UntaggedFunction::kImplicitGetter &&
|
||||
|
@ -1591,6 +1612,7 @@ void Precompiler::AddAnnotatedRoots() {
|
|||
if (function.accessor_field() == field.ptr()) {
|
||||
functions_with_entry_point_pragmas_.Insert(function);
|
||||
AddFunction(function, RetainReasons::kImplicitGetter);
|
||||
AddApiUse(function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1601,6 +1623,7 @@ void Precompiler::AddAnnotatedRoots() {
|
|||
if (function.accessor_field() == field.ptr()) {
|
||||
functions_with_entry_point_pragmas_.Insert(function);
|
||||
AddFunction(function, RetainReasons::kImplicitSetter);
|
||||
AddApiUse(function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1611,9 +1634,15 @@ void Precompiler::AddAnnotatedRoots() {
|
|||
if (function.accessor_field() == field.ptr()) {
|
||||
functions_with_entry_point_pragmas_.Insert(function);
|
||||
AddFunction(function, RetainReasons::kImplicitStaticGetter);
|
||||
AddApiUse(function);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (function.is_native()) {
|
||||
// The embedder will need to lookup this library to provide the native
|
||||
// resolver, even if there are no embedder calls into the library.
|
||||
AddApiUse(lib);
|
||||
}
|
||||
}
|
||||
|
||||
implicit_getters = GrowableObjectArray::null();
|
||||
|
@ -3069,6 +3098,166 @@ void Precompiler::DiscardCodeObjects() {
|
|||
}
|
||||
}
|
||||
|
||||
void Precompiler::PruneDictionaries() {
|
||||
// PRODUCT-only: pruning interferes with various uses of the service protocol,
|
||||
// including heap analysis tools.
|
||||
#if defined(PRODUCT)
|
||||
class PruneDictionariesVisitor {
|
||||
public:
|
||||
GrowableObjectArrayPtr PruneLibraries(
|
||||
const GrowableObjectArray& libraries) {
|
||||
for (intptr_t i = 0; i < libraries.Length(); i++) {
|
||||
lib_ ^= libraries.At(i);
|
||||
bool retain = PruneLibrary(lib_);
|
||||
if (retain) {
|
||||
lib_.set_index(retained_libraries_.Length());
|
||||
retained_libraries_.Add(lib_);
|
||||
} else {
|
||||
lib_.set_index(-1);
|
||||
lib_.set_private_key(null_string_);
|
||||
}
|
||||
}
|
||||
|
||||
Library::RegisterLibraries(Thread::Current(), retained_libraries_);
|
||||
return retained_libraries_.ptr();
|
||||
}
|
||||
|
||||
bool PruneLibrary(const Library& lib) {
|
||||
dict_ = lib.dictionary();
|
||||
intptr_t dict_size = dict_.Length() - 1;
|
||||
intptr_t used = 0;
|
||||
for (intptr_t i = 0; i < dict_size; i++) {
|
||||
entry_ = dict_.At(i);
|
||||
if (entry_.IsNull()) continue;
|
||||
|
||||
bool retain = false;
|
||||
if (entry_.IsClass()) {
|
||||
// dart:async: Fix async stack trace lookups in dart:async to annotate
|
||||
// entry points or fail gracefully.
|
||||
// dart:core, dart:collection, dart:typed_data: Isolate messaging
|
||||
// between groups allows any class in these libraries.
|
||||
retain = PruneClass(Class::Cast(entry_)) ||
|
||||
(lib.url() == Symbols::DartAsync().ptr()) ||
|
||||
(lib.url() == Symbols::DartCore().ptr()) ||
|
||||
(lib.url() == Symbols::DartCollection().ptr()) ||
|
||||
(lib.url() == Symbols::DartTypedData().ptr());
|
||||
} else if (entry_.IsFunction() || entry_.IsField()) {
|
||||
retain = precompiler_->HasApiUse(entry_);
|
||||
} else {
|
||||
FATAL("Unexpected library entry: %s", entry_.ToCString());
|
||||
}
|
||||
if (retain) {
|
||||
used++;
|
||||
} else {
|
||||
dict_.SetAt(i, Object::null_object());
|
||||
}
|
||||
}
|
||||
lib.RehashDictionary(dict_, used * 4 / 3 + 1);
|
||||
|
||||
bool retain = used > 0;
|
||||
cls_ = lib.toplevel_class();
|
||||
if (PruneClass(cls_)) {
|
||||
retain = true;
|
||||
}
|
||||
if (lib.is_dart_scheme()) {
|
||||
retain = true;
|
||||
}
|
||||
if (lib.ptr() == root_lib_.ptr()) {
|
||||
retain = true;
|
||||
}
|
||||
if (precompiler_->HasApiUse(lib)) {
|
||||
retain = true;
|
||||
}
|
||||
return retain;
|
||||
}
|
||||
|
||||
bool PruneClass(const Class& cls) {
|
||||
bool retain = precompiler_->HasApiUse(cls);
|
||||
|
||||
functions_ = cls.functions();
|
||||
retained_functions_ = GrowableObjectArray::New();
|
||||
for (intptr_t i = 0; i < functions_.Length(); i++) {
|
||||
function_ ^= functions_.At(i);
|
||||
if (precompiler_->HasApiUse(function_)) {
|
||||
retained_functions_.Add(function_);
|
||||
retain = true;
|
||||
} else if (precompiler_->functions_called_dynamically_.ContainsKey(
|
||||
function_)) {
|
||||
retained_functions_.Add(function_);
|
||||
// No `retain = true`: the function must appear in the method
|
||||
// dictionary for lookup, but the class may still be removed from the
|
||||
// library.
|
||||
}
|
||||
}
|
||||
if (retained_functions_.Length() > 0) {
|
||||
functions_ = Array::MakeFixedLength(retained_functions_);
|
||||
cls.SetFunctions(functions_);
|
||||
} else {
|
||||
cls.SetFunctions(Object::empty_array());
|
||||
}
|
||||
|
||||
fields_ = cls.fields();
|
||||
retained_fields_ = GrowableObjectArray::New();
|
||||
for (intptr_t i = 0; i < fields_.Length(); i++) {
|
||||
field_ ^= fields_.At(i);
|
||||
if (precompiler_->HasApiUse(field_)) {
|
||||
retained_fields_.Add(field_);
|
||||
retain = true;
|
||||
}
|
||||
}
|
||||
if (retained_fields_.Length() > 0) {
|
||||
fields_ = Array::MakeFixedLength(retained_fields_);
|
||||
cls.SetFields(fields_);
|
||||
} else {
|
||||
cls.SetFields(Object::empty_array());
|
||||
}
|
||||
|
||||
return retain;
|
||||
}
|
||||
|
||||
explicit PruneDictionariesVisitor(Precompiler* precompiler, Zone* zone)
|
||||
: precompiler_(precompiler),
|
||||
lib_(Library::Handle(zone)),
|
||||
dict_(Array::Handle(zone)),
|
||||
entry_(Object::Handle(zone)),
|
||||
cls_(Class::Handle(zone)),
|
||||
functions_(Array::Handle(zone)),
|
||||
fields_(Array::Handle(zone)),
|
||||
function_(Function::Handle(zone)),
|
||||
field_(Field::Handle(zone)),
|
||||
retained_functions_(GrowableObjectArray::Handle(zone)),
|
||||
retained_fields_(GrowableObjectArray::Handle(zone)),
|
||||
retained_libraries_(
|
||||
GrowableObjectArray::Handle(zone, GrowableObjectArray::New())),
|
||||
root_lib_(Library::Handle(
|
||||
zone,
|
||||
precompiler->isolate_group()->object_store()->root_library())),
|
||||
null_string_(String::Handle(zone)) {}
|
||||
|
||||
private:
|
||||
Precompiler* const precompiler_;
|
||||
Library& lib_;
|
||||
Array& dict_;
|
||||
Object& entry_;
|
||||
Class& cls_;
|
||||
Array& functions_;
|
||||
Array& fields_;
|
||||
Function& function_;
|
||||
Field& field_;
|
||||
GrowableObjectArray& retained_functions_;
|
||||
GrowableObjectArray& retained_fields_;
|
||||
const GrowableObjectArray& retained_libraries_;
|
||||
const Library& root_lib_;
|
||||
const String& null_string_;
|
||||
};
|
||||
|
||||
HANDLESCOPE(T);
|
||||
SafepointWriteRwLocker ml(T, T->isolate_group()->program_lock());
|
||||
PruneDictionariesVisitor visitor(this, Z);
|
||||
libraries_ = visitor.PruneLibraries(libraries_);
|
||||
#endif // defined(PRODUCT)
|
||||
}
|
||||
|
||||
// Traits for the HashTable template.
|
||||
struct CodeKeyTraits {
|
||||
static uint32_t Hash(const Object& key) { return Code::Cast(key).Size(); }
|
||||
|
|
|
@ -210,6 +210,37 @@ class TypeArgumentsKeyValueTrait {
|
|||
|
||||
typedef DirectChainedHashMap<TypeArgumentsKeyValueTrait> TypeArgumentsSet;
|
||||
|
||||
class ProgramElementKeyValueTrait {
|
||||
public:
|
||||
// Typedefs needed for the DirectChainedHashMap template.
|
||||
typedef const Object* Key;
|
||||
typedef const Object* Value;
|
||||
typedef const Object* Pair;
|
||||
|
||||
static Key KeyOf(Pair kv) { return kv; }
|
||||
|
||||
static Value ValueOf(Pair kv) { return kv; }
|
||||
|
||||
static inline uword Hash(Key key) {
|
||||
if (key->IsFunction()) {
|
||||
return Function::Cast(*key).Hash();
|
||||
} else if (key->IsField()) {
|
||||
return Field::Cast(*key).kernel_offset();
|
||||
} else if (key->IsClass()) {
|
||||
return Class::Cast(*key).kernel_offset();
|
||||
} else if (key->IsLibrary()) {
|
||||
return Library::Cast(*key).index();
|
||||
}
|
||||
FATAL("Unexpected type: %s\n", key->ToCString());
|
||||
}
|
||||
|
||||
static inline bool IsKeyEqual(Pair pair, Key key) {
|
||||
return pair->ptr() == key->ptr();
|
||||
}
|
||||
};
|
||||
|
||||
typedef DirectChainedHashMap<ProgramElementKeyValueTrait> ProgramElementSet;
|
||||
|
||||
class InstanceKeyValueTrait {
|
||||
public:
|
||||
// Typedefs needed for the DirectChainedHashMap template.
|
||||
|
@ -321,6 +352,8 @@ class Precompiler : public ValueObject {
|
|||
bool IsHitByTableSelector(const Function& function);
|
||||
// Returns the reason if the function must be retained, otherwise nullptr.
|
||||
const char* MustRetainFunction(const Function& function);
|
||||
void AddApiUse(const Object& obj);
|
||||
bool HasApiUse(const Object& obj);
|
||||
|
||||
void ProcessFunction(const Function& function);
|
||||
void CheckForNewDynamicFunctions();
|
||||
|
@ -343,6 +376,7 @@ class Precompiler : public ValueObject {
|
|||
void DropClasses();
|
||||
void DropLibraries();
|
||||
void DiscardCodeObjects();
|
||||
void PruneDictionaries();
|
||||
|
||||
DEBUG_ONLY(FunctionPtr FindUnvisitedRetainedFunction());
|
||||
|
||||
|
@ -393,6 +427,7 @@ class Precompiler : public ValueObject {
|
|||
TypeParameterSet typeparams_to_retain_;
|
||||
InstanceSet consts_to_retain_;
|
||||
TableSelectorSet seen_table_selectors_;
|
||||
ProgramElementSet api_uses_;
|
||||
Error& error_;
|
||||
|
||||
compiler::DispatchTableGenerator* dispatch_table_generator_;
|
||||
|
|
|
@ -1891,9 +1891,11 @@ void LoadingUnitsMetadataHelper::ReadMetadata(intptr_t node_offset) {
|
|||
Zone* zone = thread->zone();
|
||||
intptr_t unit_count = helper_->ReadUInt();
|
||||
Array& loading_units = Array::Handle(zone, Array::New(unit_count + 1));
|
||||
Array& loading_unit_uris = Array::Handle(zone, Array::New(unit_count + 1));
|
||||
LoadingUnit& unit = LoadingUnit::Handle(zone);
|
||||
LoadingUnit& parent = LoadingUnit::Handle(zone);
|
||||
Library& lib = Library::Handle(zone);
|
||||
Array& uris = Array::Handle(zone);
|
||||
|
||||
for (int i = 0; i < unit_count; i++) {
|
||||
intptr_t id = helper_->ReadUInt();
|
||||
|
@ -1907,6 +1909,7 @@ void LoadingUnitsMetadataHelper::ReadMetadata(intptr_t node_offset) {
|
|||
unit.set_parent(parent);
|
||||
|
||||
intptr_t library_count = helper_->ReadUInt();
|
||||
uris = Array::New(library_count);
|
||||
for (intptr_t j = 0; j < library_count; j++) {
|
||||
const String& uri =
|
||||
translation_helper_.DartSymbolPlain(helper_->ReadStringReference());
|
||||
|
@ -1915,14 +1918,18 @@ void LoadingUnitsMetadataHelper::ReadMetadata(intptr_t node_offset) {
|
|||
FATAL1("Missing library: %s\n", uri.ToCString());
|
||||
}
|
||||
lib.set_loading_unit(unit);
|
||||
uris.SetAt(j, uri);
|
||||
}
|
||||
|
||||
loading_units.SetAt(id, unit);
|
||||
loading_unit_uris.SetAt(id, uris);
|
||||
}
|
||||
|
||||
ObjectStore* object_store = IG->object_store();
|
||||
ASSERT(object_store->loading_units() == Array::null());
|
||||
object_store->set_loading_units(loading_units);
|
||||
ASSERT(object_store->loading_unit_uris() == Array::null());
|
||||
object_store->set_loading_unit_uris(loading_unit_uris);
|
||||
}
|
||||
|
||||
CallSiteAttributesMetadataHelper::CallSiteAttributesMetadataHelper(
|
||||
|
|
|
@ -6728,24 +6728,12 @@ DART_EXPORT Dart_Handle Dart_LoadingUnitLibraryUris(intptr_t loading_unit_id) {
|
|||
DARTSCOPE(Thread::Current());
|
||||
API_TIMELINE_DURATION(T);
|
||||
|
||||
const GrowableObjectArray& result =
|
||||
GrowableObjectArray::Handle(Z, GrowableObjectArray::New());
|
||||
const GrowableObjectArray& libs = GrowableObjectArray::Handle(
|
||||
Z, T->isolate_group()->object_store()->libraries());
|
||||
Library& lib = Library::Handle(Z);
|
||||
LoadingUnit& unit = LoadingUnit::Handle(Z);
|
||||
String& uri = String::Handle(Z);
|
||||
for (intptr_t i = 0; i < libs.Length(); i++) {
|
||||
lib ^= libs.At(i);
|
||||
unit = lib.loading_unit();
|
||||
if (unit.IsNull() || (unit.id() != loading_unit_id)) {
|
||||
continue;
|
||||
}
|
||||
uri = lib.url();
|
||||
result.Add(uri);
|
||||
const Array& loading_units =
|
||||
Array::Handle(Z, T->isolate_group()->object_store()->loading_unit_uris());
|
||||
if (loading_unit_id >= 0 && loading_unit_id < loading_units.Length()) {
|
||||
return Api::NewHandle(T, loading_units.At(loading_unit_id));
|
||||
}
|
||||
|
||||
return Api::NewHandle(T, Array::MakeFixedLength(result));
|
||||
return Api::NewError("Invalid loading_unit_id");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
|
@ -12460,8 +12460,12 @@ void Library::set_name(const String& name) const {
|
|||
untag()->set_name(name.ptr());
|
||||
}
|
||||
|
||||
void Library::set_url(const String& name) const {
|
||||
untag()->set_url(name.ptr());
|
||||
void Library::set_url(const String& url) const {
|
||||
untag()->set_url(url.ptr());
|
||||
}
|
||||
|
||||
void Library::set_private_key(const String& key) const {
|
||||
untag()->set_private_key(key.ptr());
|
||||
}
|
||||
|
||||
void Library::set_kernel_data(const ExternalTypedData& data) const {
|
||||
|
|
|
@ -5120,6 +5120,7 @@ class Library : public Object {
|
|||
// the name and url.
|
||||
void set_name(const String& name) const;
|
||||
void set_url(const String& url) const;
|
||||
void set_private_key(const String& key) const;
|
||||
|
||||
void set_num_imports(intptr_t value) const;
|
||||
void set_flags(uint8_t flags) const;
|
||||
|
|
|
@ -262,6 +262,7 @@ class ObjectPointerVisitor;
|
|||
RW(Array, dispatch_table_code_entries) \
|
||||
RW(GrowableObjectArray, instructions_tables) \
|
||||
RW(Array, obfuscation_map) \
|
||||
RW(Array, loading_unit_uris) \
|
||||
RW(GrowableObjectArray, ffi_callback_functions) \
|
||||
RW(Class, ffi_pointer_class) \
|
||||
RW(Class, ffi_native_type_class) \
|
||||
|
|
|
@ -2543,6 +2543,7 @@ Datagram _makeDatagram(
|
|||
}
|
||||
|
||||
@patch
|
||||
@pragma("vm:entry-point")
|
||||
class ResourceHandle {
|
||||
factory ResourceHandle.fromFile(RandomAccessFile file) {
|
||||
int fd = (file as _RandomAccessFile).fd;
|
||||
|
|
Loading…
Reference in a new issue