From bc7a52c960df2c6ade19eb615263bcde9e5a0ed7 Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Sun, 4 Oct 2026 19:51:11 -0700 Subject: [PATCH 1/7] [CoreAI] Add root and PTE cache clearing Summary: Add public APIs to clear Core AI cache state, either for a whole assets root or for the models referenced by one PTE. - `runtime/coreai_cache.{h,mm}`: - `clear_cache(coreai_assets_dir)` clears every keyed entry in an explicit assets root. - `clear_cache_for_pte(loader, dir)` and `clear_cache_for_pte(pte_path, dir)` clear only the entries for the current platform and architecture that the PTE's Core AI delegates reference. They find those delegates with `inspect_coreai_pte`, so no Program/Method is constructed and no Core AI assets are materialized. Every selected manifest is validated before any key is cleared. A null directory selects the backend's default root. - Per key, clearing preflights the staged source tree, evicts the SDK entry, removes staging, then removes the bookmark. The bookmark is kept until staging removal succeeds so a failed clear can be retried. SDK errors other than confirmed absence preserve the key's files. Keys are processed in sorted order and the first error is returned. Each removal syncs the directory it changed. Missing roots and entries are no-ops. - Everything under a keyed `staging/` directory belongs to the backend and is removed with that key. Outside keyed entries, PTEs, other files, lock files and unkeyed temporaries are never removed. - Supporting changes: descriptor-relative keyed staging removal in `coreai_storage`, bookmark inventory and removal in `coreai_bookmarks`, and SDK eviction plus per-key clearing in `coreai_load_coordinator`. - Tests: cache-clearing and eviction suites in `coreai_host_test`. `coreai_runtime_smoke` also links every public cache-clearing overload. - README: "Clearing cached assets". Callers must unload affected models and stop concurrent loads, inference and maintenance until these calls return. Per-key locks are not a whole-root barrier. No changes outside `backends/apple/coreai`. Test Plan: ``` cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_OSX_DEPLOYMENT_TARGET=27.0 -DEXECUTORCH_BUILD_COREAI=ON \ -DEXECUTORCH_BUILD_TESTS=ON -DEXECUTORCH_BUILD_EXTENSION_DATA_LOADER=ON cmake --build build --target backends/apple/coreai/all ctest --test-dir build/backends/apple/coreai --output-on-failure --no-tests=error ``` CTest runs `coreai_host_test` (adding the cache-clearing and eviction suites), `coreai_swift_bridge_test` and `coreai_runtime_smoke`. --- backends/apple/coreai/CMakeLists.txt | 9 + backends/apple/coreai/README.md | 56 +- backends/apple/coreai/runtime/coreai_cache.h | 57 ++ backends/apple/coreai/runtime/coreai_cache.mm | 120 +++++ .../coreai/runtime/coreai_load_coordinator.h | 12 + .../coreai/runtime/coreai_load_coordinator.mm | 63 ++- .../coreai/runtime/test/coreai_cache_test.mm | 500 ++++++++++++++++++ .../runtime/test/coreai_eviction_test.mm | 402 ++++++++++++++ .../coreai/runtime/test/coreai_fake_loader.h | 2 + .../coreai/runtime/test/coreai_fake_loader.mm | 11 +- .../coreai/runtime/test/runtime_smoke.cpp | 21 + 11 files changed, 1247 insertions(+), 6 deletions(-) create mode 100644 backends/apple/coreai/runtime/coreai_cache.h create mode 100644 backends/apple/coreai/runtime/coreai_cache.mm create mode 100644 backends/apple/coreai/runtime/test/coreai_cache_test.mm create mode 100644 backends/apple/coreai/runtime/test/coreai_eviction_test.mm diff --git a/backends/apple/coreai/CMakeLists.txt b/backends/apple/coreai/CMakeLists.txt index 83721e35ff9..ba6559b04c0 100644 --- a/backends/apple/coreai/CMakeLists.txt +++ b/backends/apple/coreai/CMakeLists.txt @@ -39,6 +39,7 @@ endif() set(_coreai_runtime_sources runtime/coreai_backend.mm runtime/coreai_bookmarks.mm + runtime/coreai_cache.mm runtime/coreai_cache_key.mm runtime/coreai_cache_root.mm runtime/coreai_error.mm @@ -87,6 +88,8 @@ if(EXECUTORCH_BUILD_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Darwin") runtime/test/coreai_delegate_test.mm runtime/test/coreai_pte_test.mm runtime/test/coreai_pte_fixture.mm + runtime/test/coreai_cache_test.mm + runtime/test/coreai_eviction_test.mm runtime/ETCoreAITensor.mm ${_coreai_runtime_sources} ) @@ -445,6 +448,12 @@ target_link_libraries( coreai_runtime_smoke PRIVATE coreaidelegate ${_coreai_runtime} ) +install( + FILES runtime/coreai_cache.h + DESTINATION + ${CMAKE_INSTALL_INCLUDEDIR}/executorch/backends/apple/coreai/runtime +) + install( TARGETS coreaidelegate coreai_swift EXPORT ExecuTorchTargets diff --git a/backends/apple/coreai/README.md b/backends/apple/coreai/README.md index c24c18bb9cd..ed337f9d469 100644 --- a/backends/apple/coreai/README.md +++ b/backends/apple/coreai/README.md @@ -229,6 +229,55 @@ clearing or after process exit, so waiters and future openers keep using the same inode. Removing the assets root externally requires all loads, sessions and maintenance to stop. +### Clearing cached assets + +The public C++ maintenance APIs are in `executorch::backends::coreai`: + +```cpp +runtime::Error clear_cache(const char* coreai_assets_dir); +runtime::Error clear_cache_for_pte( + runtime::DataLoader& pte, const char* coreai_assets_dir = nullptr); +runtime::Error clear_cache_for_pte( + const char* pte_path, const char* coreai_assets_dir = nullptr); +``` + +`clear_cache` requires an explicit absolute assets root. The PTE overloads use +user-domain `NSCachesDirectory` plus `executorch_coreai` when the directory is +omitted or null, just like backend initialization. An invalid supplied path is +an error. If loading used a custom root, pass that same root when clearing; +the PTE does not store runtime directory overrides. + +Root-wide clearing visits keyed bookmarks and staged directories. PTE clearing +selects and deduplicates keys for all Core AI delegates in every method, using +the current platform, SDK architecture and default specialization configuration. +Both public `clear_cache_for_pte` overloads use the private `inspect_coreai_pte` +reader and retain its `CoreAIPteData`, including the verified program image and +selected processed buffers, through `clear_keys`. Discovery does not load a +Program or initialize a Method, load SDK models, bind functions, extract Core AI +assets or specialize. All selected manifests are validated before any key is +cleared; root-wide clearing remains +available for existing keyed entries. Backend-local structural and semantic +verification is mandatory even with `ET_ENABLE_PROGRAM_VERIFICATION=0`. + +These synchronous calls require callers to unload affected models and prevent +concurrent loads, inference and maintenance across processes. Per-key locks are +not a whole-root barrier. Clearing first deletes the recorded SDK entry, then +staging, then its bookmark. Busy or unknown SDK errors preserve the files; +SDK-confirmed absence permits cleanup. Filesystem errors may leave partial +cleanup, but the bookmark is retained until staging removal succeeds so cleanup +can be retried. Independent keys are still attempted in sorted order, and the +first key error is returned. Each removal syncs the directory it changed. A +missing root or absent entry is a noncreating no-op. + +Everything under a keyed `staging/` directory belongs to the backend and is +removed with that key. Outside keyed entries, clearing does not delete PTEs, +other files or the stable lock files. Unkeyed interrupted-staging directories +also remain untouched. Bookmarks are SDK eviction handles, not weights: lost +bookmarks can leave SDK entries that these APIs cannot target. There is no +history or ownership registry. Copied or reused exported artifacts can share +keys; clearing them also removes the other copy's warm-cache benefit. Subsequent +loads can specialize again. + ### Runtime options Pass options through the public `Module` API before loading. In an error-returning @@ -317,8 +366,8 @@ artifact. The durable-save message confirms local bookmark publication, including its final directory flush, not independent verification of SDK-private artifact storage. Failed operations do not emit their success messages. -The delegate does not require `EXECUTORCH_BUILD_EXTENSION_DATA_LOADER`. -Enable it for consumers such as `Module` that load program data. +Enable `EXECUTORCH_BUILD_EXTENSION_DATA_LOADER` for the public PTE-file +maintenance API and consumers such as `Module`. Link the CMake target `coreaidelegate`, not just its archive filename. Its transitive dependencies supply Swift and Foundation/CoreAI linkage, and its link interface retains static backend registration. With @@ -331,7 +380,8 @@ Select the Apple SDK/toolchain when configuring the consumer project. SwiftPM and XCFramework distribution are not integrated yet. The `coreai_runtime_smoke` target checks final linkage of a C++ consumer, -including Swift dependencies and backend registration retention. With +including Swift dependencies, backend registration retention, and the public +cache-clearing overloads. With `EXECUTORCH_BUILD_TESTS=ON` it is built; otherwise it is excluded from the default build. SDK-linked tests are registered with CTest only for native macOS builds when the host meets the target's deployment minimum (at least 27.0). diff --git a/backends/apple/coreai/runtime/coreai_cache.h b/backends/apple/coreai/runtime/coreai_cache.h new file mode 100644 index 00000000000..c87dd6f9daa --- /dev/null +++ b/backends/apple/coreai/runtime/coreai_cache.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +#pragma once + +#include + +namespace executorch::runtime { +class DataLoader; +} + +namespace executorch::backends::coreai { + +/** + * Clears tracked SDK entries, keyed staging, and bookmarks in an explicit, + * dedicated Core AI assets directory. Missing roots are successful no-ops. + * PTEs, stable lock files, and unkeyed temporaries are not removed. + * SDK entries without saved bookmarks cannot be targeted. + * + * Callers must unload affected models and prevent concurrent loading, + * inference, and maintenance across processes until this call returns. + * Independent keys are attempted in sorted order, returning the first error; + * clearing is not a transaction. SDK failures preserve an entry's files unless + * the SDK confirms that entry is absent. + */ +[[nodiscard]] runtime::Error clear_cache(const char* coreai_assets_dir); + +/** + * Clears current-platform/SDK-architecture cache entries referenced by every + * CoreAI delegate in the PTE, after validating all selected manifests. Does not + * initialize delegates or materialize Core AI assets. Backend-local structural + * and semantic verification is mandatory regardless of runtime build settings. + * Reads the program region, including inline constants and unrelated inline + * blobs, and only selected Core AI external processed-data segments. + * + * A null directory selects the same Caches default as backend initialization. + * Supplied directories must be valid explicit roots; they never fall back to + * the default. The directory is never derived from the PTE's location. + * Copied PTEs can share cache keys. The quiescence and failure contract of + * clear_cache also applies. The loader and its data must remain stable for + * this call; the retained program and selected buffers survive through + * eviction. + */ +[[nodiscard]] runtime::Error clear_cache_for_pte( + runtime::DataLoader& loader, + const char* coreai_assets_dir = nullptr); + +/** FileDataLoader convenience overload with the same contract. */ +[[nodiscard]] runtime::Error clear_cache_for_pte( + const char* pte_path, + const char* coreai_assets_dir = nullptr); + +} // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_cache.mm b/backends/apple/coreai/runtime/coreai_cache.mm new file mode 100644 index 00000000000..20e07827f05 --- /dev/null +++ b/backends/apple/coreai/runtime/coreai_cache.mm @@ -0,0 +1,120 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +#include "coreai_cache.h" + +#import "coreai_assets.h" +#import "coreai_load_coordinator.h" +#include "coreai_pte.h" +#import "coreai_storage.h" + +#include +#include + +namespace executorch::backends::coreai { +namespace { +using runtime::Error; +using runtime::Result; + +Result explicit_root(const char* path) { + NSString* root = path == nullptr ? nil : [NSString stringWithUTF8String:path]; + return resolve_bookmark_root(root, false); +} + +Error clear_keys(NSString* root, NSArray* keys) { + NSArray* ordered = [[[NSSet setWithArray:keys] allObjects] + sortedArrayUsingSelector:@selector(compare:)]; + Error first_error = Error::Ok; + for (NSString* key in ordered) { + Error error = clear_bookmark_assets(root, key, nil); + if (error != Error::Ok) { + ET_LOG(Error, "Core AI cache clear failed for key %s: 0x%x", + key.UTF8String, static_cast(error)); + if (first_error == Error::Ok) first_error = error; + } + } + return first_error; +} + +Error clear_pte(runtime::DataLoader& loader, NSString* root) { + // Keep the verified image and original segment owners alive through eviction. + auto pte = inspect_coreai_pte(loader); + if (!pte.ok()) return pte.error(); + NSMutableArray* keys = [NSMutableArray array]; + NSString* architecture = nil; +#if TARGET_OS_OSX + NSString* platform = @"macOS"; +#elif TARGET_OS_IOS + NSString* platform = @"iOS"; +#else + NSString* platform = @"unsupported"; +#endif + for (const auto& buffer : pte->processed) { + auto data = buffer.data_safe(); + if (!data.ok()) return data.error(); + ET_CHECK_OR_RETURN_ERROR(data.get() != nullptr && buffer.size() > 0, + InvalidProgram, "Missing Core AI manifest"); + NSData* bytes = [NSData dataWithBytesNoCopy:const_cast(data.get()) + length:buffer.size() + freeWhenDone:NO]; + auto manifest = parse_manifest(bytes); + if (!manifest.ok()) return manifest.error(); + ET_CHECK_OR_RETURN_ERROR( + [NSProcessInfo.processInfo + isOperatingSystemAtLeastVersion:manifest->minimum_version], + DelegateInvalidCompatibility, + "Core AI model requires a newer operating system"); + if (architecture == nil) { + ET_CHECK_OR_RETURN_ERROR(ETCoreAIIsAvailable(), NotSupported, + "Core AI requires macOS 27 or iOS 27"); + architecture = ETCoreAIDeviceArchitectureName(); + } + auto selected = select_assets(manifest.get(), architecture, platform); + if (!selected.ok()) return selected.error(); + auto key = bookmark_key(selected.get(), platform, architecture); + if (!key.ok()) return key.error(); + [keys addObject:key.get()]; + } + return clear_keys(root, keys); +} +} // namespace + +Error clear_cache(const char* coreai_assets_dir) { + @autoreleasepool { + auto root = explicit_root(coreai_assets_dir); + if (!root.ok()) return root.error(); + auto keys = inventory_bookmark_keys(root.get()); + if (!keys.ok()) return keys.error(); + return clear_keys(root.get(), keys.get()); + } +} + +Error clear_cache_for_pte(runtime::DataLoader& loader, + const char* coreai_assets_dir) { + @autoreleasepool { + if (coreai_assets_dir != nullptr) { + auto root = explicit_root(coreai_assets_dir); + if (!root.ok()) return root.error(); + return clear_pte(loader, root.get()); + } + auto default_root = default_coreai_assets_root(); + if (!default_root.ok()) return default_root.error(); + auto root = resolve_bookmark_root(default_root.get(), false); + if (!root.ok()) return root.error(); + return clear_pte(loader, root.get()); + } +} + +Error clear_cache_for_pte(const char* pte_path, const char* coreai_assets_dir) { + ET_CHECK_OR_RETURN_ERROR(pte_path != nullptr && pte_path[0] != '\0', + InvalidArgument, "Missing PTE path"); + auto loader = extension::FileDataLoader::from(pte_path); + if (!loader.ok()) return loader.error(); + return clear_cache_for_pte(loader.get(), coreai_assets_dir); +} + +} // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.h b/backends/apple/coreai/runtime/coreai_load_coordinator.h index fa190084522..a3b77eb0a9c 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.h +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.h @@ -29,4 +29,16 @@ runtime::Result> acquire_bookmark_model( NSString* root, id loader); +// Deletes only the saved current SDK entry, not historical entries or sources. +runtime::Error evict_bookmark_model( + NSString* root, + NSString* key, + id loader); + +// Callers must keep affected loads and sessions quiescent across processes. +runtime::Error clear_bookmark_assets( + NSString* root, + NSString* key, + id loader); + } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.mm b/backends/apple/coreai/runtime/coreai_load_coordinator.mm index 8d7e8987498..2ac4d74c76d 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.mm +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.mm @@ -79,7 +79,31 @@ void cleanup_source(const CacheEntryLock& cache_entry) { static_cast(error)); } } -} // namespace + +Error evict_one(id loader, NSData* bookmark) { + ET_CHECK_OR_RETURN_ERROR(storage_fault(StorageOperation::BeforeEvict) == 0, + AccessFailed, "Core AI eviction interrupted"); + dispatch_semaphore_t ready = dispatch_semaphore_create(0); + __block NSError* failure = nil; + [loader evictModelWithBookmark:bookmark + completion:^(NSError* error) { + failure = error; + dispatch_semaphore_signal(ready); + }]; + dispatch_semaphore_wait(ready, DISPATCH_TIME_FOREVER); + if (failure != nil) { + // Probe after failed deletion so our own probe cannot make the entry busy. + @autoreleasepool { + auto probe = restore(loader, bookmark); + if (probe.ok() && probe.get() == nil) return Error::Ok; + } + return sdk_error(failure); + } + ET_CHECK_OR_RETURN_ERROR(storage_fault(StorageOperation::AfterEvict) == 0, + AccessFailed, "Core AI eviction interrupted"); + return Error::Ok; +} +} // namespace Result select_runtime_assets(const Manifest& manifest) { ET_CHECK_OR_RETURN_ERROR( @@ -183,4 +207,39 @@ void cleanup_source(const CacheEntryLock& cache_entry) { return model; } -} // namespace executorch::backends::coreai +Error evict_bookmark_model(NSString* root, NSString* key, + id loader) { + ET_CHECK_OR_RETURN_ERROR(loader != nil, InvalidArgument, + "Missing Core AI model loader"); + auto locked = lock_bookmark(root, key); + if (!locked.ok()) return locked.error(); + auto bookmark = read_bookmark(*locked.get()); + if (!bookmark.ok()) return bookmark.error(); + if (bookmark.get() == nil) return Error::Ok; + ET_CHECK_OK_OR_RETURN_ERROR(evict_one(loader, bookmark.get())); + return remove_bookmark(*locked.get()); +} + +Error clear_bookmark_assets(NSString* root, NSString* key, + id loader) { + auto locked = lock_bookmark(root, key, false); + if (!locked.ok()) return locked.error(); + if (locked.get() == nullptr) return Error::Ok; + auto bookmark = read_bookmark(*locked.get()); + if (!bookmark.ok()) return bookmark.error(); + ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_staging(*locked.get(), false)); + if (bookmark.get() != nil) { + if (loader == nil) { + ET_CHECK_OR_RETURN_ERROR(ETCoreAIIsAvailable(), NotSupported, + "Core AI requires macOS 27 or iOS 27"); + loader = ETCoreAICreateModelLoader(); + ET_CHECK_OR_RETURN_ERROR(loader != nil, Internal, + "Cannot create Core AI model loader"); + } + ET_CHECK_OK_OR_RETURN_ERROR(evict_one(loader, bookmark.get())); + } + ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_staging(*locked.get())); + return bookmark.get() == nil ? Error::Ok : remove_bookmark(*locked.get()); +} + +} // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/test/coreai_cache_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_test.mm new file mode 100644 index 00000000000..a6597795d14 --- /dev/null +++ b/backends/apple/coreai/runtime/test/coreai_cache_test.mm @@ -0,0 +1,500 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "coreai_cache.h" +#include +#include +#include +#include +#include +#include "coreai_bookmark_fixture.h" +#include "coreai_fake_loader.h" +#include "coreai_fault_scope.h" +#include "coreai_filesystem_fixture.h" +#include "coreai_pte_fixture.h" +#include "coreai_source_fixture.h" + +namespace executorch::backends::coreai::testing { +namespace { + +using runtime::Error; +using SegmentType = runtime::DataLoader::SegmentInfo::Type; +using ::testing::AssertionFailure; +using ::testing::AssertionResult; +using ::testing::AssertionSuccess; + +class CoreAICacheTest : public ::testing::Test { + protected: + void SetUp() override { runtime::runtime_init(); } + + void drain_callbacks() { + auto drained = bridge.wait_for_callbacks(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); + if (!drained) { + ADD_FAILURE() << drained.message(); + // Timed-out callbacks may still borrow this test's PTE or observations. + std::_Exit(EXIT_FAILURE); + } + } + + ScopedFakeBridgeState bridge; +}; + +struct CacheEntry { + NSString* key = nil; + NSData* bookmark = nil; + NSURL* stage = nil; +}; + +AssertionResult seed_cache(NSURL* root, NSDictionary* dict, BookmarkFakeLoader* loader, + CacheEntry& output, bool bookmark = true, bool stage = true, + NSString* architecture = @"arch_b") { + if (root == nil || loader == nil) { + return AssertionFailure() << "Cache seed requires a root and fake loader"; + } + auto parsed = parse_manifest(encode(dict)); + if (!parsed.ok()) { + return AssertionFailure() << "Cannot parse cache seed manifest"; + } + auto selected = select_assets(parsed.get(), architecture, @"macOS"); + if (!selected.ok()) { + return AssertionFailure() << "Cannot select cache seed assets"; + } + auto key = bookmark_key(selected.get(), @"macOS", architecture); + if (!key.ok()) { + return AssertionFailure() << "Cannot compute cache seed key"; + } + CacheEntry entry{ + key.get(), nil, + [[root URLByAppendingPathComponent:@"staging"] URLByAppendingPathComponent:key.get()]}; + auto lock = lock_bookmark(root.path, entry.key); + if (!lock.ok()) { + return AssertionFailure() << "Cannot lock cache seed bookmark"; + } + if (stage) { + auto staging = prepare_bookmark_staging(*lock.get()); + if (!staging.ok()) { + return AssertionFailure() << "Cannot prepare cache seed staging"; + } + TestData data; + if (selected->aot_compiled) { + aot_data(data, architecture); + } + auto staged = executorch::backends::coreai::prepare_source_bundle(selected.get(), &data, + staging.get(), entry.key); + if (data.requests != data.releases) { + return AssertionFailure() << "Cache seed source buffers were not released"; + } + if (!staged.ok()) { + return AssertionFailure() << "Cannot stage cache seed source bundle"; + } + } + if (bookmark) { + entry.bookmark = [NSUUID.UUID.UUIDString dataUsingEncoding:NSUTF8StringEncoding]; + BookmarkFakeModel* model = [[BookmarkFakeModel alloc] init]; + if (model == nil || entry.bookmark == nil) { + return AssertionFailure() << "Cannot create cache seed model/bookmark"; + } + model.owner = loader; + model.bookmark = entry.bookmark; + loader.models[entry.bookmark] = model; + if (write_bookmark(*lock.get(), entry.bookmark) != Error::Ok) { + return AssertionFailure() << "Cannot write cache seed bookmark"; + } + } + output = entry; + return AssertionSuccess(); +} + +bool cache_exists(NSURL* url) { return [NSFileManager.defaultManager fileExistsAtPath:url.path]; } + +void cache_entry_removed(NSURL* root, const CacheEntry& entry) { + EXPECT_FALSE(cache_exists(test_bookmark_url(root, entry.key))); + EXPECT_FALSE(cache_exists(entry.stage)); + EXPECT_TRUE(cache_exists([[root URLByAppendingPathComponent:@"locks"] + URLByAppendingPathComponent:[entry.key stringByAppendingString:@".lock"]])); +} + +NSMutableDictionary* cache_manifest(char identity) { + auto dict = manifest_dict(); + dict[@"bundle_digests"] = @{@"model.aimodel" : fixture_identity(identity)}; + return dict; +} + +TEST_F(CoreAICacheTest, EmptyAndMissingRootsAreNoncreatingNoops) { + @autoreleasepool { + BookmarkDirectory root; + ASSERT_NE(root.url, nil); + BookmarkLoaderScope fake; + ASSERT_NE(fake.loader, nil); + FBSyntheticPTE empty({{"forward", {}}}); + NSDictionary* snapshot = nil; + ASSERT_TRUE(asset_tree_snapshot(root.url, snapshot)); + const int queries = bridge.state().architecture_queries; + bridge.state().available = false; + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); + EXPECT_EQ(clear_cache_for_pte(empty, root.url.path.UTF8String), Error::Ok); + NSURL* missing = [root.url URLByAppendingPathComponent:@"absent/nested/models"]; + EXPECT_EQ(clear_cache(missing.path.UTF8String), Error::Ok); + EXPECT_EQ(clear_cache_for_pte(empty, missing.path.UTF8String), Error::Ok); + EXPECT_FALSE(cache_exists(missing)); + EXPECT_TRUE(snapshot_matches(root.url, snapshot)); + EXPECT_EQ(fake.loader->evictions.load(), 0); + EXPECT_EQ(fake.loader->restores.load(), 0); + EXPECT_EQ(fake.loader->specializations.load(), 0); + EXPECT_EQ(bridge.state().architecture_queries.load(), queries); + EXPECT_TRUE(empty.check_released()); + } +} + +TEST_F(CoreAICacheTest, ValidatesExplicitRootsAndPtePaths) { + @autoreleasepool { + BookmarkDirectory root; + ASSERT_NE(root.url, nil); + BookmarkLoaderScope fake; + ASSERT_NE(fake.loader, nil); + FBSyntheticPTE empty({{"forward", {}}}); + EXPECT_EQ(clear_cache("relative"), Error::InvalidArgument); + EXPECT_EQ(clear_cache_for_pte(empty, "relative"), Error::InvalidArgument); + EXPECT_EQ(clear_cache(nullptr), Error::InvalidArgument); + EXPECT_EQ(clear_cache_for_pte(static_cast(nullptr), root.url.path.UTF8String), + Error::InvalidArgument); + EXPECT_TRUE(empty.check_released()); + } +} + +TEST_F(CoreAICacheTest, SelectsAllMethodsDeduplicatesAndEvictsInKeyOrder) { + @autoreleasepool { + BookmarkDirectory root; + BookmarkDirectory isolated; + BookmarkLoaderScope fake; + auto source_dict = manifest_dict(); + auto second_dict = cache_manifest('2'); + auto aot_dict = aot_manifest_dict(); + CacheEntry source, second, aot, unselected, unrelated, other_root; + ASSERT_TRUE(seed_cache(root.url, source_dict, fake.loader, source)); + ASSERT_TRUE(seed_cache(root.url, second_dict, fake.loader, second)); + ASSERT_TRUE(seed_cache(root.url, aot_dict, fake.loader, aot)); + ASSERT_TRUE(seed_cache(root.url, aot_dict, fake.loader, unselected, true, true, @"arch_a")); + ASSERT_TRUE(seed_cache(root.url, cache_manifest('3'), fake.loader, unrelated)); + ASSERT_TRUE(seed_cache(isolated.url, source_dict, fake.loader, other_root)); + NSDictionary* isolation = nil; + NSDictionary* unselected_stage = nil; + NSDictionary* unrelated_stage = nil; + ASSERT_TRUE(asset_tree_snapshot(isolated.url, isolation)); + ASSERT_TRUE(asset_tree_snapshot(unselected.stage, unselected_stage)); + ASSERT_TRUE(asset_tree_snapshot(unrelated.stage, unrelated_stage)); + FBSyntheticPTE pte( + {{"empty", {}}, + {"first", + {{"UnregisteredBackend", [@"not JSON" dataUsingEncoding:NSUTF8StringEncoding], true}, + {"CoreAIBackend", encode(source_dict)}, + {"CoreAIBackend", encode(second_dict), true}}}, + {"last", + {{"CoreAIBackend", encode(source_dict), true}, + {"CoreAIBackend", encode(aot_manifest_dict()), true}}}}); + NSMutableArray* evictions = [NSMutableArray array]; + fake.loader.onEvict = ^(NSData* bookmark) { + [evictions addObject:bookmark]; + }; + const int queries = bridge.state().architecture_queries; + EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); + drain_callbacks(); + EXPECT_EQ(bridge.state().architecture_queries.load(), queries + 1); + EXPECT_EQ(fake.loader->evictions.load(), 3); + EXPECT_EQ(fake.loader->specializations.load(), 0); + EXPECT_EQ(fake.loader->bindings.load(), 0); + EXPECT_EQ(fake.loader->restores.load(), 0); + NSArray* ordered = + [@[ source.key, second.key, aot.key ] sortedArrayUsingSelector:@selector(compare:)]; + NSDictionary* bookmarks = + @{source.key : source.bookmark, second.key : second.bookmark, aot.key : aot.bookmark}; + ASSERT_EQ(evictions.count, ordered.count); + for (NSUInteger i = 0; i < ordered.count; ++i) { + SCOPED_TRACE(::testing::Message() << "sorted eviction index " << i); + EXPECT_TRUE([evictions[i] isEqual:bookmarks[ordered[i]]]); + } + cache_entry_removed(root.url, source); + cache_entry_removed(root.url, second); + cache_entry_removed(root.url, aot); + EXPECT_NE(fake.loader.models[other_root.bookmark], nil); + EXPECT_NE(fake.loader.models[unselected.bookmark], nil); + EXPECT_TRUE(snapshot_matches(isolated.url, isolation)); + EXPECT_TRUE(snapshot_matches(unselected.stage, unselected_stage)); + EXPECT_TRUE(snapshot_matches(unrelated.stage, unrelated_stage)); + EXPECT_EQ(pte.backend_indices, (std::vector{1, 2, 3})); + EXPECT_EQ(pte.backend_descriptors, + (std::vector{"CoreAIBackend", "CoreAIBackend", "CoreAIBackend"})); + EXPECT_TRUE(pte.check_released()); + } +} + +TEST_F(CoreAICacheTest, FilePathOverloadClearsReferencedEntry) { + @autoreleasepool { + BookmarkDirectory root; + BookmarkLoaderScope fake; + TestDirectory distribution; + ASSERT_NE(distribution.url, nil); + CacheEntry entry; + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, entry)); + FBSyntheticPTE pte({{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); + NSURL* path = [distribution.url URLByAppendingPathComponent:@"model.pte"]; + ASSERT_TRUE([[NSData dataWithBytes:pte.bytes.data() length:pte.bytes.size()] writeToURL:path + options:0 + error:nil]); + EXPECT_EQ(clear_cache_for_pte(path.path.UTF8String, root.url.path.UTF8String), Error::Ok); + drain_callbacks(); + EXPECT_EQ(fake.loader->evictions.load(), 1); + cache_entry_removed(root.url, entry); + } +} + +TEST_F(CoreAICacheTest, RejectsInvalidLaterDelegatesBeforeMutation) { + @autoreleasepool { + const char* rows[] = {"malformed manifest", "wrong AOT platform", "backend load failure"}; + for (int row = 0; row < 3; ++row) { + SCOPED_TRACE(rows[row]); + BookmarkDirectory root; + BookmarkLoaderScope fake; + CacheEntry source; + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, source)); + NSDictionary* snapshot = nil; + ASSERT_TRUE(asset_tree_snapshot(root.url, snapshot)); + PteDelegateSpec later{"CoreAIBackend", encode(aot_manifest_dict()), true}; + if (row == 0) later.data = [@"malformed" dataUsingEncoding:NSUTF8StringEncoding]; + if (row == 1) { + auto dict = aot_manifest_dict(); + dict[@"platform"] = @"iOS"; + later.data = encode(dict); + } + FBSyntheticPTE pte( + {{"first", {{"CoreAIBackend", encode(manifest_dict())}}}, {"later", {later}}}); + if (row == 2) { + pte.fault_call = 2; + pte.load_fault = FBSyntheticPTE::LoadFault::Error; + pte.load_error = Error::AccessFailed; + } + NSDictionary* models = [fake.loader.models copy]; + const auto error = clear_cache_for_pte(pte, root.url.path.UTF8String); + drain_callbacks(); + EXPECT_NE(error, Error::Ok); + if (row == 2) EXPECT_EQ(error, Error::AccessFailed); + EXPECT_EQ(fake.loader->evictions.load(), 0); + EXPECT_EQ(fake.loader->specializations.load(), 0); + EXPECT_EQ(fake.loader->bindings.load(), 0); + EXPECT_EQ(fake.loader->restores.load(), 0); + EXPECT_TRUE(snapshot_matches(root.url, snapshot)); + EXPECT_TRUE([fake.loader.models isEqual:models]); + EXPECT_TRUE(pte.check_released()); + } + } +} + +TEST_F(CoreAICacheTest, ProcessedBuffersRemainAliveThroughEviction) { + @autoreleasepool { + BookmarkDirectory root; + CacheEntry source; + NSData* manifest = encode(manifest_dict()); + FBSyntheticPTE valid({{"forward", + {{"CoreAIBackend", manifest, true}, + {"CoreAIBackend", manifest, true}, + {"CoreAIBackend", manifest}}}}); + // Destroy the loader scope before the PTE captured by its callback. + BookmarkLoaderScope fake; + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, source)); + __block int observations = 0; + __block bool program_alive = false; + __block int backend_releases = -1; + FBSyntheticPTE* observed = &valid; + fake.loader.onEvict = ^(NSData*) { + ++observations; + program_alive = observed->program_loads.load() == observed->program_releases.load() + 1; + backend_releases = observed->backend_releases.load(); + }; + EXPECT_EQ(clear_cache_for_pte(valid, root.url.path.UTF8String), Error::Ok); + drain_callbacks(); + EXPECT_EQ(observations, 1); + EXPECT_TRUE(program_alive); + EXPECT_EQ(backend_releases, 0); + EXPECT_EQ(valid.backend_loads.load(), 2); + EXPECT_EQ(valid.backend_indices, (std::vector{0, 1})); + EXPECT_TRUE(valid.program_alive_at_backend_release); + EXPECT_TRUE(valid.check_released()); + ASSERT_FALSE(valid.release_order.empty()); + EXPECT_EQ(valid.release_order.back(), SegmentType::Program); + } +} + +TEST_F(CoreAICacheTest, InventoriesBookmarkAndStagingUnionPreservingUnownedFiles) { + @autoreleasepool { + BookmarkDirectory root; + BookmarkLoaderScope fake; + CacheEntry both, bookmark_only, stage_only; + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, both)); + ASSERT_TRUE(seed_cache(root.url, cache_manifest('2'), fake.loader, bookmark_only, true, false)); + ASSERT_TRUE(seed_cache(root.url, cache_manifest('3'), fake.loader, stage_only, false, true)); + NSURL* temporary = [root.url URLByAppendingPathComponent:@"staging/.staging-unkeyed"]; + ASSERT_TRUE([NSFileManager.defaultManager createDirectoryAtURL:temporary + withIntermediateDirectories:NO + attributes:nil + error:nil]); + NSURL* untouched = [temporary URLByAppendingPathComponent:@"partial"]; + ASSERT_TRUE([[@"do not infer ownership" dataUsingEncoding:NSUTF8StringEncoding] + writeToURL:untouched + options:0 + error:nil]); + NSURL* external_sentinel = [root.url URLByAppendingPathComponent:@"caller.pte"]; + ASSERT_TRUE([[@"PTE" dataUsingEncoding:NSUTF8StringEncoding] writeToURL:external_sentinel + options:0 + error:nil]); + NSDictionary* temp_snapshot = nil; + NSDictionary* locks = nil; + ASSERT_TRUE(asset_tree_snapshot(temporary, temp_snapshot)); + ASSERT_TRUE(asset_tree_snapshot([root.url URLByAppendingPathComponent:@"locks"], locks)); + const int queries = bridge.state().architecture_queries; + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); + EXPECT_EQ(fake.loader->evictions.load(), 2); + EXPECT_EQ(fake.loader->restores.load(), 0); + EXPECT_EQ(fake.loader->specializations.load(), 0); + EXPECT_EQ(bridge.state().architecture_queries.load(), queries); + cache_entry_removed(root.url, both); + cache_entry_removed(root.url, bookmark_only); + cache_entry_removed(root.url, stage_only); + EXPECT_TRUE(snapshot_matches([root.url URLByAppendingPathComponent:@"locks"], locks)); + EXPECT_TRUE(snapshot_matches(temporary, temp_snapshot)); + EXPECT_TRUE(cache_exists(external_sentinel)); + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); + EXPECT_EQ(fake.loader->evictions.load(), 2); + } +} + +TEST_F(CoreAICacheTest, RemovesStagingOnlyEntriesWithoutSdkAvailability) { + @autoreleasepool { + BookmarkDirectory root; + BookmarkLoaderScope fake; + CacheEntry stage_only; + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, stage_only, false, true)); + bridge.state().available = false; + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); + cache_entry_removed(root.url, stage_only); + EXPECT_EQ(fake.loader->evictions.load(), 0); + EXPECT_EQ(fake.loader->restores.load(), 0); + } +} + +TEST_F(CoreAICacheTest, NilLoaderFactoryPreservesEntriesAndAllowsRetry) { + @autoreleasepool { + ASSERT_TRUE(bridge.state().available); + ASSERT_FALSE(bridge.state().fail_loader_factory); + BookmarkDirectory root; + BookmarkLoaderScope fake; + CacheEntry entry; + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, entry)); + NSDictionary* snapshot = nil; + ASSERT_TRUE(asset_tree_snapshot(root.url, snapshot)); + bridge.state().fail_loader_factory = true; + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Internal); + bridge.state().fail_loader_factory = false; + EXPECT_TRUE(snapshot_matches(root.url, snapshot)); + EXPECT_NE(fake.loader.models[entry.bookmark], nil); + EXPECT_EQ(fake.loader->evictions.load(), 0); + EXPECT_EQ(fake.loader->restores.load(), 0); + EXPECT_EQ(fake.loader->specializations.load(), 0); + EXPECT_EQ(fake.loader->bindings.load(), 0); + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); + cache_entry_removed(root.url, entry); + } +} + +TEST_F(CoreAICacheTest, UnavailableSdkPreservesTrackedEntriesAndAllowsRetry) { + @autoreleasepool { + BookmarkDirectory root; + BookmarkLoaderScope fake; + CacheEntry entry; + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, entry)); + NSDictionary* snapshot = nil; + ASSERT_TRUE(asset_tree_snapshot(root.url, snapshot)); + bridge.state().available = false; + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::NotSupported); + bridge.state().available = true; + EXPECT_EQ(fake.loader->evictions.load(), 0); + EXPECT_TRUE(snapshot_matches(root.url, snapshot)); + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); + cache_entry_removed(root.url, entry); + } +} + +TEST_F(CoreAICacheTest, SourceRemovalFailuresKeepBookmarkForRetry) { + @autoreleasepool { + const struct { + int match_index; + bool bookmark_removal; + const char* name; + } rows[] = {{2, false, "staged-tree removal"}, {6, true, "bookmark removal"}}; + for (const auto& row : rows) { + SCOPED_TRACE(row.name); + BookmarkDirectory root; + BookmarkLoaderScope fake; + CacheEntry entry; + ASSERT_TRUE(seed_cache(root.url, aot_manifest_dict(), fake.loader, entry)); + { + StorageFaultScope fault(StorageOperation::Remove); + fault.match_index = row.match_index; + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::AccessFailed); + EXPECT_EQ(fault.hits, 1); + } + drain_callbacks(); + auto saved = saved_bookmark(root.url, entry.key); + ASSERT_TRUE(saved.ok()); + EXPECT_TRUE([saved.get() isEqual:entry.bookmark]); + EXPECT_EQ(cache_exists(entry.stage), !row.bookmark_removal); + if (fake.loader.models[entry.bookmark] == nil) { + fake.loader.evictError = [NSError errorWithDomain:NSPOSIXErrorDomain + code:ENOENT + userInfo:nil]; + } + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); + cache_entry_removed(root.url, entry); + } + } +} + +TEST_F(CoreAICacheTest, IndependentKeysContinueInSortedOrderAfterFailures) { + @autoreleasepool { + BookmarkDirectory root; + BookmarkLoaderScope fake; + CacheEntry first, second, third; + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, first)); + ASSERT_TRUE(seed_cache(root.url, cache_manifest('2'), fake.loader, second)); + ASSERT_TRUE(seed_cache(root.url, cache_manifest('3'), fake.loader, third)); + NSArray* ordered = + [@[ first.key, second.key, third.key ] sortedArrayUsingSelector:@selector(compare:)]; + NSDictionary* bookmarks = + @{first.key : first.bookmark, second.key : second.bookmark, third.key : third.bookmark}; + NSMutableArray* evicted = [NSMutableArray array]; + BookmarkFakeLoader* loader = fake.loader; + fake.loader.onEvict = ^(NSData* bookmark) { + [evicted addObject:bookmark]; + loader.evictError = [bookmark isEqual:bookmarks[ordered[1]]] + ? [NSError errorWithDomain:NSPOSIXErrorDomain code:EBUSY userInfo:nil] + : nil; + }; + StorageFaultScope fault(StorageOperation::BeforeEvict); + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::AccessFailed); + drain_callbacks(); + EXPECT_EQ(fault.hits, 1); + ASSERT_EQ(evicted.count, 2u); + EXPECT_TRUE([evicted[0] isEqual:bookmarks[ordered[1]]]); + EXPECT_TRUE([evicted[1] isEqual:bookmarks[ordered[2]]]); + EXPECT_TRUE(cache_exists(test_bookmark_url(root.url, ordered[0]))); + EXPECT_TRUE(cache_exists(test_bookmark_url(root.url, ordered[1]))); + EXPECT_FALSE(cache_exists(test_bookmark_url(root.url, ordered[2]))); + } +} + +} // namespace +} // namespace executorch::backends::coreai::testing diff --git a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm new file mode 100644 index 00000000000..619da5e1754 --- /dev/null +++ b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm @@ -0,0 +1,402 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include "coreai_bookmark_fixture.h" +#include "coreai_fake_loader.h" +#include "coreai_fault_scope.h" +#include "coreai_load_coordinator.h" +#include "coreai_source_fixture.h" + +using namespace executorch::runtime; +using namespace executorch::backends::coreai; +using namespace executorch::backends::coreai::testing; + +namespace { +NSError* bookmark_test_error(NSInteger code = EIO) { + return [NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:nil]; +} + +// These cases never retain a delegate handle beyond initialization. +Error initialize_and_destroy(NSDictionary* dict, const NamedDataMap* data, NSString* root) { + if (root == nil) return Error::InvalidArgument; + BackendOptions<1> options; + auto error = options.set_option("coreai_assets_dir", root.UTF8String); + if (error != Error::Ok) return error; + auto* backend = get_backend_class("CoreAIBackend"); + if (backend == nullptr) return Error::Internal; + BackendInitContext context(nullptr, nullptr, "forward", data, + {options.view().data(), options.view().size()}); + NSData* json = encode(dict); + if (json == nil) return Error::InvalidArgument; + FreeableBuffer processed(json.bytes, json.length, nullptr); + auto handle = backend->init(context, &processed, {}); + if (!handle.ok()) return handle.error(); + backend->destroy(handle.get()); + return Error::Ok; +} + +dispatch_time_t deadline() { return dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC); } + +class CoreAIEvictionTest : public ::testing::Test { + protected: + ScopedFakeBridgeState bridge; + void SetUp() override { runtime_init(); } +}; + +// Release the callback before waiting; never unwind storage borrowed by a hang. +struct ReleaseAndJoin final { + dispatch_semaphore_t gate; + dispatch_semaphore_t eviction_done; + dispatch_semaphore_t loading_done; + ScopedFakeBridgeState& bridge; + pthread_t eviction{}; + pthread_t loading{}; + bool eviction_started = false; + bool loading_started = false; + bool released = false; + bool joined = false; + long eviction_wait = -1; + long loading_wait = -1; + + void release() { + if (!released) { + dispatch_semaphore_signal(gate); + released = true; + } + } + void join() { + release(); + if (joined) return; + const auto completion_deadline = deadline(); + if (eviction_started) + eviction_wait = dispatch_semaphore_wait(eviction_done, completion_deadline); + if (loading_started) loading_wait = dispatch_semaphore_wait(loading_done, completion_deadline); + if ((eviction_started && eviction_wait != 0) || (loading_started && loading_wait != 0)) { + ADD_FAILURE() << "Eviction/load workers did not return after releasing the gate"; + std::_Exit(EXIT_FAILURE); + } + const int eviction_join = eviction_started ? pthread_join(eviction, nullptr) : 0; + const int loading_join = loading_started ? pthread_join(loading, nullptr) : 0; + if (eviction_join != 0 || loading_join != 0) { + ADD_FAILURE() << "Cannot join eviction/load workers: " << eviction_join << ", " + << loading_join; + std::_Exit(EXIT_FAILURE); + } + auto drained = bridge.wait_for_callbacks(deadline()); + if (!drained) { + ADD_FAILURE() << drained.message(); + std::_Exit(EXIT_FAILURE); + } + joined = true; + } + ~ReleaseAndJoin() { join(); } +}; +} // namespace + +@interface PinBookmarkLoader : BookmarkFakeLoader +@property(nonatomic, weak) BookmarkFakeModel* temporary; +@property(nonatomic, copy) ETCoreAIAcquisitionCompletion retainedCompletion; +@end + +@implementation PinBookmarkLoader +- (void)specializeModelAtURL:(NSURL*)url completion:(ETCoreAIAcquisitionCompletion)completion { + ++specializations; + self.retainedCompletion = completion; + @autoreleasepool { + BookmarkFakeModel* model = [[BookmarkFakeModel alloc] init]; + model.owner = self; + model.bookmark = [@"pinned-bookmark" dataUsingEncoding:NSUTF8StringEncoding]; + self.temporary = model; + completion(ETCoreAIAcquisitionStatusHit, model, nil); + model = nil; + } +} +- (void)restoreModelFromBookmark:(NSData*)bookmark + completion:(ETCoreAIAcquisitionCompletion)completion { + ++restores; + self.retainedCompletion = completion; + @autoreleasepool { + BookmarkFakeModel* model = self.temporary; + completion(model == nil ? ETCoreAIAcquisitionStatusMiss : ETCoreAIAcquisitionStatusHit, model, + nil); + model = nil; + } +} +- (void)evictModelWithBookmark:(NSData*)bookmark completion:(void (^)(NSError*))completion { + ++evictions; + bool busy; + @autoreleasepool { + busy = self.temporary != nil; + } + completion(busy ? bookmark_test_error(EBUSY) : nil); +} +- (void)clearRetainedState { + self.retainedCompletion = nil; + [super clearRetainedState]; +} +@end + +namespace { +TEST_F(CoreAIEvictionTest, ConfirmedMissRecoveryEvictsOnlyCurrentBookmark) { + @autoreleasepool { + BookmarkDirectory root; + ASSERT_NE(root.url, nil); + BookmarkLoaderScope fake; + ASSERT_NE(fake.loader, nil); + TestData data; + auto selected = bookmark_manifest(); + ASSERT_TRUE(selected.ok()); + auto key_result = test_bookmark_key(selected.get()); + ASSERT_TRUE(key_result.ok()); + NSString* key = key_result.get(); + NSMutableDictionary* dict = manifest_dict(); + ASSERT_EQ(initialize_and_destroy(dict, &data, root.url.path), Error::Ok); + auto original_result = saved_bookmark(root.url, key); + ASSERT_TRUE(original_result.ok()); + NSData* original = original_result.get(); + ASSERT_NE(original, nil); + NSString* source_path = fake.loader.lastSpecializeURL; + ASSERT_NE(source_path, nil); + NSURL* source_url = [NSURL fileURLWithPath:source_path]; + EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + + fake.loader.restoreMiss = YES; + fake.loader.specializeError = bookmark_test_error(); + EXPECT_NE(initialize_and_destroy(dict, &data, root.url.path), Error::Ok); + auto after_failure = saved_bookmark(root.url, key); + ASSERT_TRUE(after_failure.ok()); + EXPECT_TRUE([after_failure.get() isEqual:original]); + EXPECT_TRUE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + fake.loader.specializeError = nil; + ASSERT_EQ(initialize_and_destroy(dict, &data, root.url.path), Error::Ok); + EXPECT_EQ(fake.loader->specializations.load(), 3); + EXPECT_EQ(fake.loader->evictions.load(), 0); + EXPECT_TRUE([fake.loader.lastSpecializeURL isEqual:source_path]); + auto saved_result = saved_bookmark(root.url, key); + ASSERT_TRUE(saved_result.ok()); + NSData* saved = saved_result.get(); + ASSERT_NE(saved, nil); + EXPECT_FALSE([saved isEqual:original]); + EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + ASSERT_EQ(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + EXPECT_NE(fake.loader.models[original], nil); + EXPECT_EQ(fake.loader.models[saved], nil); + EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + fake.loader.restoreMiss = NO; + fake.loader.emptyBookmark = YES; + EXPECT_NE(initialize_and_destroy(dict, &data, root.url.path), Error::Ok); + auto empty = saved_bookmark(root.url, key); + ASSERT_TRUE(empty.ok()); + EXPECT_EQ(empty.get(), nil); + fake.loader.emptyBookmark = NO; + ASSERT_EQ(initialize_and_destroy(dict, &data, root.url.path), Error::Ok); + EXPECT_EQ(fake.loader->specializations.load(), 5); + EXPECT_EQ(data.requests.load(), data.releases.load()); + } +} + +TEST_F(CoreAIEvictionTest, BusyAndUnknownEvictionRetainBookmarkUntilConfirmedAbsent) { + @autoreleasepool { + BookmarkDirectory root; + ASSERT_NE(root.url, nil); + BookmarkLoaderScope fake; + ASSERT_NE(fake.loader, nil); + TestData data; + auto selected = bookmark_manifest(); + ASSERT_TRUE(selected.ok()); + auto key_result = test_bookmark_key(selected.get()); + ASSERT_TRUE(key_result.ok()); + NSString* key = key_result.get(); + EXPECT_EQ(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + EXPECT_EQ(fake.loader->evictions.load(), 0); + ASSERT_EQ(initialize_and_destroy(manifest_dict(), &data, root.url.path), Error::Ok); + auto saved_result = saved_bookmark(root.url, key); + ASSERT_TRUE(saved_result.ok()); + NSData* saved = saved_result.get(); + ASSERT_NE(saved, nil); + NSURL* source_url = + [[[root.url URLByAppendingPathComponent:@"staging"] URLByAppendingPathComponent:key] + URLByAppendingPathComponent:selected->path.lastPathComponent]; + EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + for (bool probe_error : {false, true}) { + SCOPED_TRACE(probe_error ? "unknown restore failure" : "restore hit/busy"); + fake.loader.evictError = bookmark_test_error(EBUSY); + fake.loader.restoreError = probe_error ? bookmark_test_error() : nil; + EXPECT_NE(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + auto retained = saved_bookmark(root.url, key); + ASSERT_TRUE(retained.ok()); + EXPECT_TRUE([retained.get() isEqual:saved]); + EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + } + fake.loader.restoreError = nil; + fake.loader.evictError = nil; + { + StorageFaultScope fault(StorageOperation::AfterEvict); + EXPECT_NE(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + EXPECT_EQ(fault.hits, 1); + } + auto retained = saved_bookmark(root.url, key); + ASSERT_TRUE(retained.ok()); + EXPECT_TRUE([retained.get() isEqual:saved]); + fake.loader.evictError = bookmark_test_error(ENOENT); + ASSERT_EQ(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + auto absent = saved_bookmark(root.url, key); + ASSERT_TRUE(absent.ok()); + EXPECT_EQ(absent.get(), nil); + EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + fake.loader.evictError = nil; + ASSERT_EQ(initialize_and_destroy(manifest_dict(), &data, root.url.path), Error::Ok); + { + StorageFaultScope fault(StorageOperation::Remove); + EXPECT_NE(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + EXPECT_EQ(fault.hits, 1); + auto before_remove = saved_bookmark(root.url, key); + ASSERT_TRUE(before_remove.ok()); + EXPECT_NE(before_remove.get(), nil); + } + ASSERT_EQ(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + auto removed = saved_bookmark(root.url, key); + ASSERT_TRUE(removed.ok()); + EXPECT_EQ(removed.get(), nil); + EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + EXPECT_EQ(data.requests.load(), data.releases.load()); + } +} + +TEST_F(CoreAIEvictionTest, PreparedPinsBlockEvictionWithoutRetainedCompletionLeak) { + @autoreleasepool { + BookmarkDirectory root; + ASSERT_NE(root.url, nil); + TestData data; + auto selected = bookmark_manifest(); + ASSERT_TRUE(selected.ok()); + auto key_result = test_bookmark_key(selected.get()); + ASSERT_TRUE(key_result.ok()); + NSString* key = key_result.get(); + PinBookmarkLoader* loader = [[PinBookmarkLoader alloc] init]; + ASSERT_NE(loader, nil); + // Scope teardown clears retained completions even after fatal test + // failures. + bridge.state().bookmark_loader = loader; + @autoreleasepool { + auto pinned = + acquire_bookmark_model(selected.get(), &data, root.url.path, @"macOS", @"arch_b", loader); + ASSERT_TRUE(pinned.ok()); + EXPECT_NE(loader.temporary, nil); + EXPECT_NE(evict_bookmark_model(root.url.path, key, loader), Error::Ok); + auto saved = saved_bookmark(root.url, key); + ASSERT_TRUE(saved.ok()); + EXPECT_NE(saved.get(), nil); + } + EXPECT_EQ(loader.temporary, nil); + EXPECT_NE(loader.retainedCompletion, nil); + ASSERT_EQ(evict_bookmark_model(root.url.path, key, loader), Error::Ok); + auto removed = saved_bookmark(root.url, key); + ASSERT_TRUE(removed.ok()); + EXPECT_EQ(removed.get(), nil); + EXPECT_EQ(loader->evictions.load(), 2); + NSURL* source_url = + [[[root.url URLByAppendingPathComponent:@"staging"] URLByAppendingPathComponent:key] + URLByAppendingPathComponent:selected->path.lastPathComponent]; + EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + [loader clearRetainedState]; + EXPECT_EQ(data.requests.load(), data.releases.load()); + } +} + +TEST_F(CoreAIEvictionTest, LoadContendsOnActualKeyLockUntilEvictionFinishes) { + @autoreleasepool { + BookmarkDirectory root; + ASSERT_NE(root.url, nil); + BookmarkLoaderScope fake; + ASSERT_NE(fake.loader, nil); + TestData data; + auto selected = bookmark_manifest(); + ASSERT_TRUE(selected.ok()); + auto key_result = test_bookmark_key(selected.get()); + ASSERT_TRUE(key_result.ok()); + NSString* key = key_result.get(); + ASSERT_EQ(initialize_and_destroy(manifest_dict(), &data, root.url.path), Error::Ok); + dispatch_semaphore_t deleting = dispatch_semaphore_create(0); + dispatch_semaphore_t proceed = dispatch_semaphore_create(0); + dispatch_semaphore_t loading_observed = dispatch_semaphore_create(0); + dispatch_semaphore_t eviction_done = dispatch_semaphore_create(0); + dispatch_semaphore_t loading_done = dispatch_semaphore_create(0); + __block long gate_wait = -1; + fake.loader.onEvict = ^(NSData*) { + dispatch_semaphore_signal(deleting); + gate_wait = dispatch_semaphore_wait(proceed, deadline()); + }; + Error eviction_error = Error::Internal; + bool load_ok = false; + auto evict = [&] { + @autoreleasepool { + eviction_error = evict_bookmark_model(root.url.path, key, fake.loader); + } + dispatch_semaphore_signal(eviction_done); + }; + auto load = [&] { + @autoreleasepool { + auto acquired = acquire_bookmark_model(selected.get(), &data, root.url.path, @"macOS", + @"arch_b", fake.loader); + load_ok = acquired.ok() && acquired.get() != nil; + } + dispatch_semaphore_signal(loading_observed); + dispatch_semaphore_signal(loading_done); + }; + ReleaseAndJoin cleanup{proceed, eviction_done, loading_done, bridge}; + const int eviction_start = pthread_create( + &cleanup.eviction, nullptr, + [](void* context) -> void* { + (*static_cast(context))(); + return nullptr; + }, + &evict); + cleanup.eviction_started = eviction_start == 0; + const long entered = + cleanup.eviction_started ? dispatch_semaphore_wait(deleting, deadline()) : -1; + int loading_start = -1; + long premature_completion = -1; + if (entered == 0) { + loading_start = pthread_create( + &cleanup.loading, nullptr, + [](void* context) -> void* { + (*static_cast(context))(); + return nullptr; + }, + &load); + cleanup.loading_started = loading_start == 0; + if (cleanup.loading_started) { + premature_completion = dispatch_semaphore_wait( + loading_observed, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC)); + } + } + cleanup.join(); + fake.loader.onEvict = nil; + + EXPECT_EQ(eviction_start, 0); + EXPECT_EQ(loading_start, 0); + EXPECT_EQ(entered, 0); + EXPECT_NE(premature_completion, 0); + EXPECT_EQ(gate_wait, 0); + EXPECT_EQ(cleanup.eviction_wait, 0); + EXPECT_EQ(cleanup.loading_wait, 0); + EXPECT_EQ(eviction_error, Error::Ok); + EXPECT_TRUE(load_ok); + EXPECT_EQ(fake.loader->specializations.load(), 2); + auto saved = saved_bookmark(root.url, key); + ASSERT_TRUE(saved.ok()); + EXPECT_NE(saved.get(), nil); + EXPECT_EQ(data.requests.load(), data.releases.load()); + } +} +} // namespace diff --git a/backends/apple/coreai/runtime/test/coreai_fake_loader.h b/backends/apple/coreai/runtime/test/coreai_fake_loader.h index b5194100429..6f03c9f78b1 100644 --- a/backends/apple/coreai/runtime/test/coreai_fake_loader.h +++ b/backends/apple/coreai/runtime/test/coreai_fake_loader.h @@ -97,9 +97,11 @@ class ScopedFakeBridgeState final { @property(nonatomic, strong) NSError* restoreError; @property(nonatomic, strong) NSError* specializeError; @property(nonatomic, strong) NSError* bindError; +@property(nonatomic, strong) NSError* evictError; @property(nonatomic) BOOL restoreMiss; @property(nonatomic) BOOL emptyBookmark; @property(nonatomic, copy) void (^onSpecialize)(NSURL*); +@property(nonatomic, copy) void (^onEvict)(NSData*); @property(nonatomic, strong) BookmarkFakeModel* lastAcquired; @property(nonatomic, strong) BookmarkFakeModel* lastBound; @property(nonatomic, copy) NSString* lastSpecializeURL; diff --git a/backends/apple/coreai/runtime/test/coreai_fake_loader.mm b/backends/apple/coreai/runtime/test/coreai_fake_loader.mm index 019552e978d..7540d60fe74 100644 --- a/backends/apple/coreai/runtime/test/coreai_fake_loader.mm +++ b/backends/apple/coreai/runtime/test/coreai_fake_loader.mm @@ -435,11 +435,20 @@ - (void)evictModelWithBookmark:(NSData*)bookmark completion:(void (^)(NSError*))completion { ++evictions; run_async(self.context, ^{ - completion(fake_error(ETCoreAIErrorUnsupported)); + if (self.onEvict != nil) { + self.onEvict(bookmark); + } + @synchronized(self) { + if (self.evictError == nil) { + [self.models removeObjectForKey:bookmark]; + } + completion(self.evictError); + } }); } - (void)clearRetainedState { self.onSpecialize = nil; + self.onEvict = nil; self.lastAcquired = nil; self.lastBound = nil; [self.models removeAllObjects]; diff --git a/backends/apple/coreai/runtime/test/runtime_smoke.cpp b/backends/apple/coreai/runtime/test/runtime_smoke.cpp index 3d91a760b4b..a136dceb1df 100644 --- a/backends/apple/coreai/runtime/test/runtime_smoke.cpp +++ b/backends/apple/coreai/runtime/test/runtime_smoke.cpp @@ -6,13 +6,34 @@ * LICENSE file in the root directory of this source tree. */ +#include #include #include #include +namespace { +using executorch::runtime::DataLoader; +using executorch::runtime::Error; +using RootClear = Error (*)(const char*); +using FileClear = Error (*)(const char*, const char*); +using LoaderClear = Error (*)(DataLoader&, const char*); + +// Keep every overload in the link check without touching cache storage. +RootClear volatile clear_root = &executorch::backends::coreai::clear_cache; +FileClear volatile clear_file = + &executorch::backends::coreai::clear_cache_for_pte; +LoaderClear volatile clear_loader = + &executorch::backends::coreai::clear_cache_for_pte; +} // namespace + int main() { executorch::runtime::runtime_init(); + if (clear_root == nullptr || clear_file == nullptr || + clear_loader == nullptr) { + std::fputs("Core AI cache APIs were not linked\n", stderr); + return 1; + } const auto* backend = executorch::runtime::get_backend_class("CoreAIBackend"); if (backend == nullptr) { std::fputs("CoreAIBackend was not retained by the linker\n", stderr); From ed31fdc91116b22eed7d045a4e66fbe302958881 Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Mon, 26 Oct 2026 17:54:33 -0700 Subject: [PATCH 2/7] [CoreAI] Align cache clearing with loading and cover production eviction Summary: Prevent loading and maintenance from drifting in how they select model assets, and let applications clear the default cache without reconstructing a private path. Exercise real clearing behavior for eviction failures and loaded models while preserving strict compatibility checks and storage validation. Simplify the public maintenance documentation. Test Plan: Built the CoreAI targets and ran all four CTest suites successfully, including native host tests, Swift bridge tests, SDK source-lifetime tests, and runtime smoke coverage. New tests verify portable/AOT load-to-clear identity, isolated default-root clearing, rejection before mutation, and production eviction failure/retry behavior. Ran the complete CoreAI Python suite: 113 passed. Whitespace checks passed. The generic validator could not complete its Buck ownership query; native and Swift validation passed through CMake. --- backends/apple/coreai/README.md | 23 ++++----- backends/apple/coreai/runtime/coreai_cache.h | 21 ++++----- backends/apple/coreai/runtime/coreai_cache.mm | 47 +++++++------------ .../coreai/runtime/coreai_load_coordinator.h | 6 --- .../coreai/runtime/coreai_load_coordinator.mm | 13 ----- .../coreai/runtime/test/coreai_cache_test.mm | 21 +++++++-- .../runtime/test/coreai_eviction_test.mm | 42 ++++++++++++----- 7 files changed, 82 insertions(+), 91 deletions(-) diff --git a/backends/apple/coreai/README.md b/backends/apple/coreai/README.md index ed337f9d469..5c6c84685e5 100644 --- a/backends/apple/coreai/README.md +++ b/backends/apple/coreai/README.md @@ -234,30 +234,25 @@ and maintenance to stop. The public C++ maintenance APIs are in `executorch::backends::coreai`: ```cpp -runtime::Error clear_cache(const char* coreai_assets_dir); +runtime::Error clear_cache(const char* coreai_assets_dir = nullptr); runtime::Error clear_cache_for_pte( runtime::DataLoader& pte, const char* coreai_assets_dir = nullptr); runtime::Error clear_cache_for_pte( const char* pte_path, const char* coreai_assets_dir = nullptr); ``` -`clear_cache` requires an explicit absolute assets root. The PTE overloads use -user-domain `NSCachesDirectory` plus `executorch_coreai` when the directory is -omitted or null, just like backend initialization. An invalid supplied path is -an error. If loading used a custom root, pass that same root when clearing; -the PTE does not store runtime directory overrides. +All three APIs use the same default assets directory as backend initialization +when the directory is omitted or null. A supplied path must be absolute; invalid +paths fail without falling back to the default. If loading used a custom root, +pass that same root when clearing; the PTE does not store runtime overrides. Root-wide clearing visits keyed bookmarks and staged directories. PTE clearing selects and deduplicates keys for all Core AI delegates in every method, using the current platform, SDK architecture and default specialization configuration. -Both public `clear_cache_for_pte` overloads use the private `inspect_coreai_pte` -reader and retain its `CoreAIPteData`, including the verified program image and -selected processed buffers, through `clear_keys`. Discovery does not load a -Program or initialize a Method, load SDK models, bind functions, extract Core AI -assets or specialize. All selected manifests are validated before any key is -cleared; root-wide clearing remains -available for existing keyed entries. Backend-local structural and semantic -verification is mandatory even with `ET_ENABLE_PROGRAM_VERIFICATION=0`. +All selected manifests must be valid and compatible before any entry is cleared. +Incompatible delegates are not silently skipped. Root-wide clearing remains +available independently of the PTE. Discovery does not load models or materialize +assets. These synchronous calls require callers to unload affected models and prevent concurrent loads, inference and maintenance across processes. Per-key locks are diff --git a/backends/apple/coreai/runtime/coreai_cache.h b/backends/apple/coreai/runtime/coreai_cache.h index c87dd6f9daa..2e1d28bbf28 100644 --- a/backends/apple/coreai/runtime/coreai_cache.h +++ b/backends/apple/coreai/runtime/coreai_cache.h @@ -16,8 +16,9 @@ class DataLoader; namespace executorch::backends::coreai { /** - * Clears tracked SDK entries, keyed staging, and bookmarks in an explicit, - * dedicated Core AI assets directory. Missing roots are successful no-ops. + * Clears tracked SDK entries, keyed staging, and bookmarks in a dedicated + * Core AI assets directory. Null selects the same default as loading; supplied + * paths must be absolute. Missing roots are successful no-ops. * PTEs, stable lock files, and unkeyed temporaries are not removed. * SDK entries without saved bookmarks cannot be targeted. * @@ -27,23 +28,21 @@ namespace executorch::backends::coreai { * clearing is not a transaction. SDK failures preserve an entry's files unless * the SDK confirms that entry is absent. */ -[[nodiscard]] runtime::Error clear_cache(const char* coreai_assets_dir); +[[nodiscard]] runtime::Error clear_cache( + const char* coreai_assets_dir = nullptr); /** * Clears current-platform/SDK-architecture cache entries referenced by every - * CoreAI delegate in the PTE, after validating all selected manifests. Does not - * initialize delegates or materialize Core AI assets. Backend-local structural - * and semantic verification is mandatory regardless of runtime build settings. - * Reads the program region, including inline constants and unrelated inline - * blobs, and only selected Core AI external processed-data segments. + * CoreAI delegate in the PTE, after validating all selected manifests. Invalid + * or incompatible delegates fail before any entries are cleared. Does not + * initialize delegates or materialize Core AI assets. * * A null directory selects the same Caches default as backend initialization. * Supplied directories must be valid explicit roots; they never fall back to * the default. The directory is never derived from the PTE's location. * Copied PTEs can share cache keys. The quiescence and failure contract of - * clear_cache also applies. The loader and its data must remain stable for - * this call; the retained program and selected buffers survive through - * eviction. + * clear_cache also applies. Keep the loader alive and its data unchanged until + * the call returns. */ [[nodiscard]] runtime::Error clear_cache_for_pte( runtime::DataLoader& loader, diff --git a/backends/apple/coreai/runtime/coreai_cache.mm b/backends/apple/coreai/runtime/coreai_cache.mm index 20e07827f05..06c23a118d8 100644 --- a/backends/apple/coreai/runtime/coreai_cache.mm +++ b/backends/apple/coreai/runtime/coreai_cache.mm @@ -12,7 +12,6 @@ #include "coreai_pte.h" #import "coreai_storage.h" -#include #include namespace executorch::backends::coreai { @@ -20,8 +19,16 @@ using runtime::Error; using runtime::Result; -Result explicit_root(const char* path) { - NSString* root = path == nullptr ? nil : [NSString stringWithUTF8String:path]; +Result cache_root(const char* path) { + NSString* root = nil; + if (path == nullptr) { + auto default_root = default_coreai_assets_root(); + if (!default_root.ok()) + return default_root.error(); + root = default_root.get(); + } else { + root = [NSString stringWithUTF8String:path]; + } return resolve_bookmark_root(root, false); } @@ -46,13 +53,6 @@ Error clear_pte(runtime::DataLoader& loader, NSString* root) { if (!pte.ok()) return pte.error(); NSMutableArray* keys = [NSMutableArray array]; NSString* architecture = nil; -#if TARGET_OS_OSX - NSString* platform = @"macOS"; -#elif TARGET_OS_IOS - NSString* platform = @"iOS"; -#else - NSString* platform = @"unsupported"; -#endif for (const auto& buffer : pte->processed) { auto data = buffer.data_safe(); if (!data.ok()) return data.error(); @@ -63,19 +63,11 @@ Error clear_pte(runtime::DataLoader& loader, NSString* root) { freeWhenDone:NO]; auto manifest = parse_manifest(bytes); if (!manifest.ok()) return manifest.error(); - ET_CHECK_OR_RETURN_ERROR( - [NSProcessInfo.processInfo - isOperatingSystemAtLeastVersion:manifest->minimum_version], - DelegateInvalidCompatibility, - "Core AI model requires a newer operating system"); - if (architecture == nil) { - ET_CHECK_OR_RETURN_ERROR(ETCoreAIIsAvailable(), NotSupported, - "Core AI requires macOS 27 or iOS 27"); - architecture = ETCoreAIDeviceArchitectureName(); - } - auto selected = select_assets(manifest.get(), architecture, platform); + auto selected = select_runtime_assets(manifest.get(), architecture); if (!selected.ok()) return selected.error(); - auto key = bookmark_key(selected.get(), platform, architecture); + architecture = selected->architecture; + auto key = + bookmark_key(selected->manifest, selected->platform, architecture); if (!key.ok()) return key.error(); [keys addObject:key.get()]; } @@ -85,7 +77,7 @@ Error clear_pte(runtime::DataLoader& loader, NSString* root) { Error clear_cache(const char* coreai_assets_dir) { @autoreleasepool { - auto root = explicit_root(coreai_assets_dir); + auto root = cache_root(coreai_assets_dir); if (!root.ok()) return root.error(); auto keys = inventory_bookmark_keys(root.get()); if (!keys.ok()) return keys.error(); @@ -96,14 +88,7 @@ Error clear_cache(const char* coreai_assets_dir) { Error clear_cache_for_pte(runtime::DataLoader& loader, const char* coreai_assets_dir) { @autoreleasepool { - if (coreai_assets_dir != nullptr) { - auto root = explicit_root(coreai_assets_dir); - if (!root.ok()) return root.error(); - return clear_pte(loader, root.get()); - } - auto default_root = default_coreai_assets_root(); - if (!default_root.ok()) return default_root.error(); - auto root = resolve_bookmark_root(default_root.get(), false); + auto root = cache_root(coreai_assets_dir); if (!root.ok()) return root.error(); return clear_pte(loader, root.get()); } diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.h b/backends/apple/coreai/runtime/coreai_load_coordinator.h index a3b77eb0a9c..03d59f3cde2 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.h +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.h @@ -29,12 +29,6 @@ runtime::Result> acquire_bookmark_model( NSString* root, id loader); -// Deletes only the saved current SDK entry, not historical entries or sources. -runtime::Error evict_bookmark_model( - NSString* root, - NSString* key, - id loader); - // Callers must keep affected loads and sessions quiescent across processes. runtime::Error clear_bookmark_assets( NSString* root, diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.mm b/backends/apple/coreai/runtime/coreai_load_coordinator.mm index 2ac4d74c76d..78e9d756f22 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.mm +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.mm @@ -207,19 +207,6 @@ Error evict_one(id loader, NSData* bookmark) { return model; } -Error evict_bookmark_model(NSString* root, NSString* key, - id loader) { - ET_CHECK_OR_RETURN_ERROR(loader != nil, InvalidArgument, - "Missing Core AI model loader"); - auto locked = lock_bookmark(root, key); - if (!locked.ok()) return locked.error(); - auto bookmark = read_bookmark(*locked.get()); - if (!bookmark.ok()) return bookmark.error(); - if (bookmark.get() == nil) return Error::Ok; - ET_CHECK_OK_OR_RETURN_ERROR(evict_one(loader, bookmark.get())); - return remove_bookmark(*locked.get()); -} - Error clear_bookmark_assets(NSString* root, NSString* key, id loader) { auto locked = lock_bookmark(root, key, false); diff --git a/backends/apple/coreai/runtime/test/coreai_cache_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_test.mm index a6597795d14..5878c574e07 100644 --- a/backends/apple/coreai/runtime/test/coreai_cache_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_test.mm @@ -160,7 +160,8 @@ void cache_entry_removed(NSURL* root, const CacheEntry& entry) { FBSyntheticPTE empty({{"forward", {}}}); EXPECT_EQ(clear_cache("relative"), Error::InvalidArgument); EXPECT_EQ(clear_cache_for_pte(empty, "relative"), Error::InvalidArgument); - EXPECT_EQ(clear_cache(nullptr), Error::InvalidArgument); + EXPECT_EQ(clear_cache(""), Error::InvalidArgument); + EXPECT_EQ(clear_cache("\xff"), Error::InvalidArgument); EXPECT_EQ(clear_cache_for_pte(static_cast(nullptr), root.url.path.UTF8String), Error::InvalidArgument); EXPECT_TRUE(empty.check_released()); @@ -255,8 +256,13 @@ FBSyntheticPTE pte( TEST_F(CoreAICacheTest, RejectsInvalidLaterDelegatesBeforeMutation) { @autoreleasepool { - const char* rows[] = {"malformed manifest", "wrong AOT platform", "backend load failure"}; - for (int row = 0; row < 3; ++row) { + const char* rows[] = { + "malformed manifest", + "wrong AOT platform", + "backend load failure", + "newer OS", + "missing architecture"}; + for (int row = 0; row < 5; ++row) { SCOPED_TRACE(rows[row]); BookmarkDirectory root; BookmarkLoaderScope fake; @@ -271,6 +277,13 @@ FBSyntheticPTE pte( dict[@"platform"] = @"iOS"; later.data = encode(dict); } + if (row == 3) { + auto dict = aot_manifest_dict(); + dict[@"min_deployment_version"] = @"999999.0"; + later.data = encode(dict); + } + if (row == 4) + bridge.state().device_architecture = @"missing_arch"; FBSyntheticPTE pte( {{"first", {{"CoreAIBackend", encode(manifest_dict())}}}, {"later", {later}}}); if (row == 2) { @@ -283,6 +296,8 @@ FBSyntheticPTE pte( drain_callbacks(); EXPECT_NE(error, Error::Ok); if (row == 2) EXPECT_EQ(error, Error::AccessFailed); + if (row == 1 || row >= 3) + EXPECT_EQ(error, Error::DelegateInvalidCompatibility); EXPECT_EQ(fake.loader->evictions.load(), 0); EXPECT_EQ(fake.loader->specializations.load(), 0); EXPECT_EQ(fake.loader->bindings.load(), 0); diff --git a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm index 619da5e1754..9cf065e9fc8 100644 --- a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm @@ -187,7 +187,8 @@ - (void)clearRetainedState { ASSERT_NE(saved, nil); EXPECT_FALSE([saved isEqual:original]); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); - ASSERT_EQ(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + ASSERT_EQ( + clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); EXPECT_NE(fake.loader.models[original], nil); EXPECT_EQ(fake.loader.models[saved], nil); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); @@ -216,9 +217,17 @@ - (void)clearRetainedState { auto key_result = test_bookmark_key(selected.get()); ASSERT_TRUE(key_result.ok()); NSString* key = key_result.get(); - EXPECT_EQ(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + EXPECT_EQ( + clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); EXPECT_EQ(fake.loader->evictions.load(), 0); - ASSERT_EQ(initialize_and_destroy(manifest_dict(), &data, root.url.path), Error::Ok); + // Retain staging after publication to exercise clearing's failure paths. + { + StorageFaultScope fault(StorageOperation::Remove); + ASSERT_EQ( + initialize_and_destroy(manifest_dict(), &data, root.url.path), + Error::AccessFailed); + EXPECT_EQ(fault.hits, 1); + } auto saved_result = saved_bookmark(root.url, key); ASSERT_TRUE(saved_result.ok()); NSData* saved = saved_result.get(); @@ -226,29 +235,34 @@ - (void)clearRetainedState { NSURL* source_url = [[[root.url URLByAppendingPathComponent:@"staging"] URLByAppendingPathComponent:key] URLByAppendingPathComponent:selected->path.lastPathComponent]; - EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + EXPECT_TRUE( + [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); for (bool probe_error : {false, true}) { SCOPED_TRACE(probe_error ? "unknown restore failure" : "restore hit/busy"); fake.loader.evictError = bookmark_test_error(EBUSY); fake.loader.restoreError = probe_error ? bookmark_test_error() : nil; - EXPECT_NE(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + EXPECT_NE( + clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); auto retained = saved_bookmark(root.url, key); ASSERT_TRUE(retained.ok()); EXPECT_TRUE([retained.get() isEqual:saved]); - EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + EXPECT_TRUE( + [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); } fake.loader.restoreError = nil; fake.loader.evictError = nil; { StorageFaultScope fault(StorageOperation::AfterEvict); - EXPECT_NE(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + EXPECT_NE( + clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); EXPECT_EQ(fault.hits, 1); } auto retained = saved_bookmark(root.url, key); ASSERT_TRUE(retained.ok()); EXPECT_TRUE([retained.get() isEqual:saved]); fake.loader.evictError = bookmark_test_error(ENOENT); - ASSERT_EQ(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + ASSERT_EQ( + clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); auto absent = saved_bookmark(root.url, key); ASSERT_TRUE(absent.ok()); EXPECT_EQ(absent.get(), nil); @@ -257,13 +271,15 @@ - (void)clearRetainedState { ASSERT_EQ(initialize_and_destroy(manifest_dict(), &data, root.url.path), Error::Ok); { StorageFaultScope fault(StorageOperation::Remove); - EXPECT_NE(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + EXPECT_NE( + clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); EXPECT_EQ(fault.hits, 1); auto before_remove = saved_bookmark(root.url, key); ASSERT_TRUE(before_remove.ok()); EXPECT_NE(before_remove.get(), nil); } - ASSERT_EQ(evict_bookmark_model(root.url.path, key, fake.loader), Error::Ok); + ASSERT_EQ( + clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); auto removed = saved_bookmark(root.url, key); ASSERT_TRUE(removed.ok()); EXPECT_EQ(removed.get(), nil); @@ -292,14 +308,14 @@ - (void)clearRetainedState { acquire_bookmark_model(selected.get(), &data, root.url.path, @"macOS", @"arch_b", loader); ASSERT_TRUE(pinned.ok()); EXPECT_NE(loader.temporary, nil); - EXPECT_NE(evict_bookmark_model(root.url.path, key, loader), Error::Ok); + EXPECT_NE(clear_bookmark_assets(root.url.path, key, loader), Error::Ok); auto saved = saved_bookmark(root.url, key); ASSERT_TRUE(saved.ok()); EXPECT_NE(saved.get(), nil); } EXPECT_EQ(loader.temporary, nil); EXPECT_NE(loader.retainedCompletion, nil); - ASSERT_EQ(evict_bookmark_model(root.url.path, key, loader), Error::Ok); + ASSERT_EQ(clear_bookmark_assets(root.url.path, key, loader), Error::Ok); auto removed = saved_bookmark(root.url, key); ASSERT_TRUE(removed.ok()); EXPECT_EQ(removed.get(), nil); @@ -340,7 +356,7 @@ - (void)clearRetainedState { bool load_ok = false; auto evict = [&] { @autoreleasepool { - eviction_error = evict_bookmark_model(root.url.path, key, fake.loader); + eviction_error = clear_bookmark_assets(root.url.path, key, fake.loader); } dispatch_semaphore_signal(eviction_done); }; From e1d36cb4cf1fe9aa1ec3bfad3bb04582b85c261e Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Tue, 27 Oct 2026 15:17:44 -0700 Subject: [PATCH 3/7] [CoreAI] Clear all recorded PTE cache variants safely Clear every recorded variant associated with a PTE regardless of host loading compatibility. Persist durable per-key attribution without changing existing cache keys, and report incomplete cleanup when older entries cannot be safely identified. Recover malformed local entries without following links, while preserving uncertain SDK eviction state for retry. Test Plan: Ran all five CoreAI CTest suites: 236 native tests, 18 Swift tests, PTE export, real SDK source-lifetime checks, and runtime smoke all passed. Coverage includes cross-platform and architecture variants, legacy attribution, malformed entries, publication/removal failures, retry durability, and preservation of outside files. Ran the complete CoreAI Python suite: 124 tests passed. Formatting and whitespace checks passed. The generic Buck Swift owner lookup still fails without diagnostics; CMake build and functional validation passed. --- backends/apple/coreai/CMakeLists.txt | 4 +- backends/apple/coreai/README.md | 53 +- backends/apple/coreai/runtime/coreai_cache.h | 11 +- backends/apple/coreai/runtime/coreai_cache.mm | 128 +++- .../coreai/runtime/coreai_load_coordinator.mm | 71 ++- .../coreai/runtime/test/coreai_cache_test.mm | 583 ++++++++++++++---- .../runtime/test/coreai_eviction_test.mm | 174 ++++-- tools/cmake/preset/default.cmake | 4 + 8 files changed, 794 insertions(+), 234 deletions(-) diff --git a/backends/apple/coreai/CMakeLists.txt b/backends/apple/coreai/CMakeLists.txt index ba6559b04c0..a802b8987b6 100644 --- a/backends/apple/coreai/CMakeLists.txt +++ b/backends/apple/coreai/CMakeLists.txt @@ -422,8 +422,8 @@ coreai_require_os27(coreaidelegate) target_include_directories(coreaidelegate PUBLIC ${_common_include_directories}) add_dependencies(coreaidelegate coreai_swift) target_link_libraries( - coreaidelegate PRIVATE coreai_swift ${_coreai_runtime} program_schema - executorch::coreai_dependencies + coreaidelegate PRIVATE coreai_swift ${_coreai_runtime} ${_coreai_data_loader} + program_schema executorch::coreai_dependencies ) executorch_target_link_options_shared_lib(coreaidelegate) if(EXECUTORCH_BUILD_SHARED) diff --git a/backends/apple/coreai/README.md b/backends/apple/coreai/README.md index 5c6c84685e5..f5d8f6ddd97 100644 --- a/backends/apple/coreai/README.md +++ b/backends/apple/coreai/README.md @@ -246,13 +246,14 @@ when the directory is omitted or null. A supplied path must be absolute; invalid paths fail without falling back to the default. If loading used a custom root, pass that same root when clearing; the PTE does not store runtime overrides. -Root-wide clearing visits keyed bookmarks and staged directories. PTE clearing -selects and deduplicates keys for all Core AI delegates in every method, using -the current platform, SDK architecture and default specialization configuration. -All selected manifests must be valid and compatible before any entry is cleared. -Incompatible delegates are not silently skipped. Root-wide clearing remains -available independently of the PTE. Discovery does not load models or materialize -assets. +Root-wide clearing visits keyed bookmarks, identity records, and staged +directories. PTE clearing finds every Core AI delegate in every method and clears +all recorded variants of its bundles, including other platforms and SDK +architectures. Manifests must be structurally valid before any entry is cleared; +their minimum OS and runtime compatibility do not restrict maintenance. +Discovery requires neither SDK availability nor an architecture query, and does +not load models or materialize assets. Actual SDK eviction still requires a +usable bookmark and an available loader. These synchronous calls require callers to unload affected models and prevent concurrent loads, inference and maintenance across processes. Per-key locks are @@ -267,11 +268,39 @@ missing root or absent entry is a noncreating no-op. Everything under a keyed `staging/` directory belongs to the backend and is removed with that key. Outside keyed entries, clearing does not delete PTEs, other files or the stable lock files. Unkeyed interrupted-staging directories -also remain untouched. Bookmarks are SDK eviction handles, not weights: lost -bookmarks can leave SDK entries that these APIs cannot target. There is no -history or ownership registry. Copied or reused exported artifacts can share -keys; clearing them also removes the other copy's warm-cache benefit. Subsequent -loads can specialize again. +also remain untouched. Bookmarks are SDK eviction handles, not weights. For an +existing entry with a missing or structurally invalid bookmark, clearing logs an +Info message with the cache key, attempts local cleanup, and continues to other keys. +The message states that SDK eviction could not be confirmed; bookmark contents +are not logged. Absent entries remain quiet no-ops. I/O failures and SDK eviction +errors still preserve bookmarks for retry. Lost bookmarks can leave SDK entries +that these APIs cannot target. No history of removed entries is retained. +Copied or reused exported artifacts can share keys; clearing them also removes +the other copy's warm-cache benefit. Subsequent loads can specialize again. + +Identity records are small JSON sidecars beside bookmarks. They retain bundle +identity and cache context, and are checked against their hashed key. Loading +publishes the record before staging or SDK acquisition, and clearing removes it +last. Existing keys and opaque bookmark bytes are unchanged. Older AOT entries +can be found directly from the PTE's architecture list. Older source entries +without attribution cannot always be identified: PTE clearing removes known +matches, logs unresolved keys, and reports incomplete cleanup rather than +removing unrelated entries. A one-time root-wide `clear_cache` handles these +older entries. Missing or corrupt attribution has the same conservative behavior. + +### Persistent SDK Rejection + +SDK errors are not classified as transient or permanent. +Busy errors and all unclassified eviction/restore errors retain the local +bookmark for retry; only successful eviction or a confirmed SDK miss permits +normal removal. If retries keep failing, first stop every process using the +assets root. Back up or quarantine only the affected key's bookmark, identity +sidecar, and keyed staging outside that root, leaving the stable lock files in +place. A later load can attempt cold acquisition and create new tracking data. +This manual recovery does not confirm SDK eviction or guarantee disk-space +reclamation: keep the backup because it may contain the only remaining eviction +handle. Do not perform manual recovery while loading, inference, or maintenance +is running. ### Runtime options diff --git a/backends/apple/coreai/runtime/coreai_cache.h b/backends/apple/coreai/runtime/coreai_cache.h index 2e1d28bbf28..b6ce1c06258 100644 --- a/backends/apple/coreai/runtime/coreai_cache.h +++ b/backends/apple/coreai/runtime/coreai_cache.h @@ -32,10 +32,13 @@ namespace executorch::backends::coreai { const char* coreai_assets_dir = nullptr); /** - * Clears current-platform/SDK-architecture cache entries referenced by every - * CoreAI delegate in the PTE, after validating all selected manifests. Invalid - * or incompatible delegates fail before any entries are cleared. Does not - * initialize delegates or materialize Core AI assets. + * Clears all recorded cache variants referenced by every CoreAI delegate in + * the PTE after validating its manifests. Discovery does not require host + * compatibility or SDK availability. Actual SDK eviction requires a loader. + * Missing/corrupt attribution can leave legacy source entries unidentified: + * known matches are cleared, then an error reports incomplete cleanup. A + * root-wide clear can recover these entries. Does not initialize delegates or + * materialize Core AI assets. * * A null directory selects the same Caches default as backend initialization. * Supplied directories must be valid explicit roots; they never fall back to diff --git a/backends/apple/coreai/runtime/coreai_cache.mm b/backends/apple/coreai/runtime/coreai_cache.mm index 06c23a118d8..f20155dd843 100644 --- a/backends/apple/coreai/runtime/coreai_cache.mm +++ b/backends/apple/coreai/runtime/coreai_cache.mm @@ -39,9 +39,13 @@ Error clear_keys(NSString* root, NSArray* keys) { for (NSString* key in ordered) { Error error = clear_bookmark_assets(root, key, nil); if (error != Error::Ok) { - ET_LOG(Error, "Core AI cache clear failed for key %s: 0x%x", - key.UTF8String, static_cast(error)); - if (first_error == Error::Ok) first_error = error; + ET_LOG( + Error, + "Core AI cache clear failed for key %s: 0x%x", + key.UTF8String, + static_cast(error)); + if (first_error == Error::Ok) + first_error = error; } } return first_error; @@ -50,56 +54,120 @@ Error clear_keys(NSString* root, NSArray* keys) { Error clear_pte(runtime::DataLoader& loader, NSString* root) { // Keep the verified image and original segment owners alive through eviction. auto pte = inspect_coreai_pte(loader); - if (!pte.ok()) return pte.error(); - NSMutableArray* keys = [NSMutableArray array]; - NSString* architecture = nil; + if (!pte.ok()) + return pte.error(); + NSMutableSet* identities = [NSMutableSet set]; + NSMutableSet* legacy_aot_keys = [NSMutableSet set]; + bool has_source = false; for (const auto& buffer : pte->processed) { auto data = buffer.data_safe(); - if (!data.ok()) return data.error(); - ET_CHECK_OR_RETURN_ERROR(data.get() != nullptr && buffer.size() > 0, - InvalidProgram, "Missing Core AI manifest"); + if (!data.ok()) + return data.error(); + ET_CHECK_OR_RETURN_ERROR( + data.get() != nullptr && buffer.size() > 0, + InvalidProgram, + "Missing Core AI manifest"); NSData* bytes = [NSData dataWithBytesNoCopy:const_cast(data.get()) - length:buffer.size() - freeWhenDone:NO]; + length:buffer.size() + freeWhenDone:NO]; auto manifest = parse_manifest(bytes); - if (!manifest.ok()) return manifest.error(); - auto selected = select_runtime_assets(manifest.get(), architecture); - if (!selected.ok()) return selected.error(); - architecture = selected->architecture; - auto key = - bookmark_key(selected->manifest, selected->platform, architecture); - if (!key.ok()) return key.error(); - [keys addObject:key.get()]; + if (!manifest.ok()) + return manifest.error(); + for (NSString* bundle in manifest->bundle_digests) { + auto identity = bookmark_identity(manifest.get(), bundle); + if (!identity.ok()) + return identity.error(); + [identities addObject:identity.get()]; + } + if (manifest->aot_compiled) { + for (NSString* architecture in manifest->archs) { + auto selected = + select_assets(manifest.get(), architecture, manifest->platform); + if (!selected.ok()) + return selected.error(); + auto key = + bookmark_key(selected.get(), manifest->platform, architecture); + if (!key.ok()) + return key.error(); + [legacy_aot_keys addObject:key.get()]; + } + } else { + has_source = true; + } + } + if (identities.count == 0) + return Error::Ok; + auto inventory = inventory_bookmark_keys(root); + if (!inventory.ok()) + return inventory.error(); + NSMutableArray* keys = [NSMutableArray array]; + Error lookup_error = Error::Ok; + for (NSString* key in inventory.get()) { + if ([legacy_aot_keys containsObject:key]) { + [keys addObject:key]; + continue; + } + auto locked = lock_bookmark(root, key, false); + if (!locked.ok()) { + if (lookup_error == Error::Ok) + lookup_error = locked.error(); + continue; + } + if (locked.get() == nullptr) + continue; + auto identity = read_bookmark_identity(*locked.get()); + if (!identity.ok() || identity.get() == nil) { + if (has_source) { + ET_LOG( + Info, + "Core AI cache key %s has missing or invalid attribution; PTE cleanup is incomplete", + key.UTF8String); + if (lookup_error == Error::Ok) + lookup_error = + identity.ok() ? Error::InvalidExternalData : identity.error(); + } + continue; + } + if ([identities containsObject:identity.get()]) + [keys addObject:key]; } - return clear_keys(root, keys); + const auto cleared = clear_keys(root, keys); + return lookup_error != Error::Ok ? lookup_error : cleared; } -} // namespace +} // namespace Error clear_cache(const char* coreai_assets_dir) { @autoreleasepool { auto root = cache_root(coreai_assets_dir); - if (!root.ok()) return root.error(); + if (!root.ok()) + return root.error(); auto keys = inventory_bookmark_keys(root.get()); - if (!keys.ok()) return keys.error(); + if (!keys.ok()) + return keys.error(); return clear_keys(root.get(), keys.get()); } } -Error clear_cache_for_pte(runtime::DataLoader& loader, - const char* coreai_assets_dir) { +Error clear_cache_for_pte( + runtime::DataLoader& loader, + const char* coreai_assets_dir) { @autoreleasepool { auto root = cache_root(coreai_assets_dir); - if (!root.ok()) return root.error(); + if (!root.ok()) + return root.error(); return clear_pte(loader, root.get()); } } Error clear_cache_for_pte(const char* pte_path, const char* coreai_assets_dir) { - ET_CHECK_OR_RETURN_ERROR(pte_path != nullptr && pte_path[0] != '\0', - InvalidArgument, "Missing PTE path"); + ET_CHECK_OR_RETURN_ERROR( + pte_path != nullptr && pte_path[0] != '\0', + InvalidArgument, + "Missing PTE path"); auto loader = extension::FileDataLoader::from(pte_path); - if (!loader.ok()) return loader.error(); + if (!loader.ok()) + return loader.error(); return clear_cache_for_pte(loader.get(), coreai_assets_dir); } -} // namespace executorch::backends::coreai +} // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.mm b/backends/apple/coreai/runtime/coreai_load_coordinator.mm index 78e9d756f22..4532a159e86 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.mm +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.mm @@ -81,8 +81,10 @@ void cleanup_source(const CacheEntryLock& cache_entry) { } Error evict_one(id loader, NSData* bookmark) { - ET_CHECK_OR_RETURN_ERROR(storage_fault(StorageOperation::BeforeEvict) == 0, - AccessFailed, "Core AI eviction interrupted"); + ET_CHECK_OR_RETURN_ERROR( + storage_fault(StorageOperation::BeforeEvict) == 0, + AccessFailed, + "Core AI eviction interrupted"); dispatch_semaphore_t ready = dispatch_semaphore_create(0); __block NSError* failure = nil; [loader evictModelWithBookmark:bookmark @@ -95,15 +97,18 @@ Error evict_one(id loader, NSData* bookmark) { // Probe after failed deletion so our own probe cannot make the entry busy. @autoreleasepool { auto probe = restore(loader, bookmark); - if (probe.ok() && probe.get() == nil) return Error::Ok; + if (probe.ok() && probe.get() == nil) + return Error::Ok; } return sdk_error(failure); } - ET_CHECK_OR_RETURN_ERROR(storage_fault(StorageOperation::AfterEvict) == 0, - AccessFailed, "Core AI eviction interrupted"); + ET_CHECK_OR_RETURN_ERROR( + storage_fault(StorageOperation::AfterEvict) == 0, + AccessFailed, + "Core AI eviction interrupted"); return Error::Ok; } -} // namespace +} // namespace Result select_runtime_assets(const Manifest& manifest) { ET_CHECK_OR_RETURN_ERROR( @@ -207,26 +212,48 @@ Error evict_one(id loader, NSData* bookmark) { return model; } -Error clear_bookmark_assets(NSString* root, NSString* key, - id loader) { +Error clear_bookmark_assets( + NSString* root, + NSString* key, + id loader) { auto locked = lock_bookmark(root, key, false); - if (!locked.ok()) return locked.error(); - if (locked.get() == nullptr) return Error::Ok; - auto bookmark = read_bookmark(*locked.get()); - if (!bookmark.ok()) return bookmark.error(); - ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_staging(*locked.get(), false)); - if (bookmark.get() != nil) { + if (!locked.ok()) + return locked.error(); + if (locked.get() == nullptr) + return Error::Ok; + auto bookmark = read_bookmark(*locked.get(), /*maintenance=*/true); + if (!bookmark.ok() && bookmark.error() != Error::InvalidExternalData) { + return bookmark.error(); + } + NSData* bytes = bookmark.ok() ? bookmark.get() : nil; + if (bytes == nil) { + ET_LOG( + Info, + "Core AI cache key %s has a %s bookmark; SDK eviction could not be " + "confirmed; attempting local cleanup", + key.UTF8String, + bookmark.ok() ? "missing" : "corrupt"); + } + ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_staging( + *locked.get(), false, StorageRemovalMode::Maintenance)); + if (bytes != nil) { if (loader == nil) { - ET_CHECK_OR_RETURN_ERROR(ETCoreAIIsAvailable(), NotSupported, - "Core AI requires macOS 27 or iOS 27"); + ET_CHECK_OR_RETURN_ERROR( + ETCoreAIIsAvailable(), + NotSupported, + "Core AI requires macOS 27 or iOS 27"); loader = ETCoreAICreateModelLoader(); - ET_CHECK_OR_RETURN_ERROR(loader != nil, Internal, - "Cannot create Core AI model loader"); + ET_CHECK_OR_RETURN_ERROR( + loader != nil, Internal, "Cannot create Core AI model loader"); } - ET_CHECK_OK_OR_RETURN_ERROR(evict_one(loader, bookmark.get())); + ET_CHECK_OK_OR_RETURN_ERROR(evict_one(loader, bytes)); + } + ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_staging( + *locked.get(), true, StorageRemovalMode::Maintenance)); + if (!bookmark.ok() || bytes != nil) { + ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark(*locked.get())); } - ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_staging(*locked.get())); - return bookmark.get() == nil ? Error::Ok : remove_bookmark(*locked.get()); + return remove_bookmark_identity(*locked.get()); } -} // namespace executorch::backends::coreai +} // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/test/coreai_cache_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_test.mm index 5878c574e07..c9e55500c03 100644 --- a/backends/apple/coreai/runtime/test/coreai_cache_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_test.mm @@ -6,13 +6,19 @@ * LICENSE file in the root directory of this source tree. */ -#include "coreai_cache.h" +#include #include +#include #include +#include +#include #include #include +#include +#include #include #include "coreai_bookmark_fixture.h" +#include "coreai_cache.h" #include "coreai_fake_loader.h" #include "coreai_fault_scope.h" #include "coreai_filesystem_fixture.h" @@ -30,10 +36,13 @@ class CoreAICacheTest : public ::testing::Test { protected: - void SetUp() override { runtime::runtime_init(); } + void SetUp() override { + runtime::runtime_init(); + } void drain_callbacks() { - auto drained = bridge.wait_for_callbacks(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); + auto drained = bridge.wait_for_callbacks( + dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); if (!drained) { ADD_FAILURE() << drained.message(); // Timed-out callbacks may still borrow this test's PTE or observations. @@ -50,9 +59,16 @@ void drain_callbacks() { NSURL* stage = nil; }; -AssertionResult seed_cache(NSURL* root, NSDictionary* dict, BookmarkFakeLoader* loader, - CacheEntry& output, bool bookmark = true, bool stage = true, - NSString* architecture = @"arch_b") { +AssertionResult seed_cache( + NSURL* root, + NSDictionary* dict, + BookmarkFakeLoader* loader, + CacheEntry& output, + bool bookmark = true, + bool stage = true, + NSString* architecture = @"arch_b", + NSString* platform = @"macOS", + bool metadata = true) { if (root == nil || loader == nil) { return AssertionFailure() << "Cache seed requires a root and fake loader"; } @@ -60,21 +76,28 @@ AssertionResult seed_cache(NSURL* root, NSDictionary* dict, BookmarkFakeLoader* if (!parsed.ok()) { return AssertionFailure() << "Cannot parse cache seed manifest"; } - auto selected = select_assets(parsed.get(), architecture, @"macOS"); + auto selected = select_assets(parsed.get(), architecture, platform); if (!selected.ok()) { return AssertionFailure() << "Cannot select cache seed assets"; } - auto key = bookmark_key(selected.get(), @"macOS", architecture); + auto key = bookmark_key(selected.get(), platform, architecture); if (!key.ok()) { return AssertionFailure() << "Cannot compute cache seed key"; } CacheEntry entry{ - key.get(), nil, - [[root URLByAppendingPathComponent:@"staging"] URLByAppendingPathComponent:key.get()]}; + key.get(), + nil, + [[root URLByAppendingPathComponent:@"staging"] + URLByAppendingPathComponent:key.get()]}; auto lock = lock_bookmark(root.path, entry.key); if (!lock.ok()) { return AssertionFailure() << "Cannot lock cache seed bookmark"; } + if (metadata && + ensure_bookmark_identity( + *lock.get(), selected.get(), platform, architecture) != Error::Ok) { + return AssertionFailure() << "Cannot attribute cache seed"; + } if (stage) { auto staging = prepare_bookmark_staging(*lock.get()); if (!staging.ok()) { @@ -84,17 +107,19 @@ AssertionResult seed_cache(NSURL* root, NSDictionary* dict, BookmarkFakeLoader* if (selected->aot_compiled) { aot_data(data, architecture); } - auto staged = executorch::backends::coreai::prepare_source_bundle(selected.get(), &data, - staging.get(), entry.key); + auto staged = executorch::backends::coreai::prepare_source_bundle( + selected.get(), &data, staging.get(), entry.key); if (data.requests != data.releases) { - return AssertionFailure() << "Cache seed source buffers were not released"; + return AssertionFailure() + << "Cache seed source buffers were not released"; } if (!staged.ok()) { return AssertionFailure() << "Cannot stage cache seed source bundle"; } } if (bookmark) { - entry.bookmark = [NSUUID.UUID.UUIDString dataUsingEncoding:NSUTF8StringEncoding]; + entry.bookmark = + [NSUUID.UUID.UUIDString dataUsingEncoding:NSUTF8StringEncoding]; BookmarkFakeModel* model = [[BookmarkFakeModel alloc] init]; if (model == nil || entry.bookmark == nil) { return AssertionFailure() << "Cannot create cache seed model/bookmark"; @@ -110,13 +135,22 @@ AssertionResult seed_cache(NSURL* root, NSDictionary* dict, BookmarkFakeLoader* return AssertionSuccess(); } -bool cache_exists(NSURL* url) { return [NSFileManager.defaultManager fileExistsAtPath:url.path]; } +bool cache_exists(NSURL* url) { + return [NSFileManager.defaultManager fileExistsAtPath:url.path]; +} + +NSURL* cache_identity_url(NSURL* root, NSString* key) { + return [[test_bookmark_url(root, key) URLByDeletingPathExtension] + URLByAppendingPathExtension:@"metadata"]; +} void cache_entry_removed(NSURL* root, const CacheEntry& entry) { + EXPECT_FALSE(cache_exists(cache_identity_url(root, entry.key))); EXPECT_FALSE(cache_exists(test_bookmark_url(root, entry.key))); EXPECT_FALSE(cache_exists(entry.stage)); EXPECT_TRUE(cache_exists([[root URLByAppendingPathComponent:@"locks"] - URLByAppendingPathComponent:[entry.key stringByAppendingString:@".lock"]])); + URLByAppendingPathComponent:[entry.key + stringByAppendingString:@".lock"]])); } NSMutableDictionary* cache_manifest(char identity) { @@ -127,7 +161,7 @@ void cache_entry_removed(NSURL* root, const CacheEntry& entry) { TEST_F(CoreAICacheTest, EmptyAndMissingRootsAreNoncreatingNoops) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; ASSERT_NE(root.url, nil); BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); @@ -138,9 +172,15 @@ void cache_entry_removed(NSURL* root, const CacheEntry& entry) { bridge.state().available = false; EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); EXPECT_EQ(clear_cache_for_pte(empty, root.url.path.UTF8String), Error::Ok); - NSURL* missing = [root.url URLByAppendingPathComponent:@"absent/nested/models"]; + NSURL* missing = + [root.url URLByAppendingPathComponent:@"absent/nested/models"]; EXPECT_EQ(clear_cache(missing.path.UTF8String), Error::Ok); EXPECT_EQ(clear_cache_for_pte(empty, missing.path.UTF8String), Error::Ok); + FBSyntheticPTE pte( + {{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); + EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); + EXPECT_EQ(clear_cache_for_pte(pte, missing.path.UTF8String), Error::Ok); + EXPECT_TRUE(pte.check_released()); EXPECT_FALSE(cache_exists(missing)); EXPECT_TRUE(snapshot_matches(root.url, snapshot)); EXPECT_EQ(fake.loader->evictions.load(), 0); @@ -153,7 +193,7 @@ void cache_entry_removed(NSURL* root, const CacheEntry& entry) { TEST_F(CoreAICacheTest, ValidatesExplicitRootsAndPtePaths) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; ASSERT_NE(root.url, nil); BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); @@ -162,37 +202,41 @@ void cache_entry_removed(NSURL* root, const CacheEntry& entry) { EXPECT_EQ(clear_cache_for_pte(empty, "relative"), Error::InvalidArgument); EXPECT_EQ(clear_cache(""), Error::InvalidArgument); EXPECT_EQ(clear_cache("\xff"), Error::InvalidArgument); - EXPECT_EQ(clear_cache_for_pte(static_cast(nullptr), root.url.path.UTF8String), - Error::InvalidArgument); + EXPECT_EQ( + clear_cache_for_pte( + static_cast(nullptr), root.url.path.UTF8String), + Error::InvalidArgument); EXPECT_TRUE(empty.check_released()); } } TEST_F(CoreAICacheTest, SelectsAllMethodsDeduplicatesAndEvictsInKeyOrder) { @autoreleasepool { - BookmarkDirectory root; - BookmarkDirectory isolated; + TestDirectory root; + TestDirectory isolated; BookmarkLoaderScope fake; auto source_dict = manifest_dict(); auto second_dict = cache_manifest('2'); auto aot_dict = aot_manifest_dict(); - CacheEntry source, second, aot, unselected, unrelated, other_root; + CacheEntry source, second, aot, other_arch, unrelated, other_root; ASSERT_TRUE(seed_cache(root.url, source_dict, fake.loader, source)); ASSERT_TRUE(seed_cache(root.url, second_dict, fake.loader, second)); ASSERT_TRUE(seed_cache(root.url, aot_dict, fake.loader, aot)); - ASSERT_TRUE(seed_cache(root.url, aot_dict, fake.loader, unselected, true, true, @"arch_a")); - ASSERT_TRUE(seed_cache(root.url, cache_manifest('3'), fake.loader, unrelated)); + ASSERT_TRUE(seed_cache( + root.url, aot_dict, fake.loader, other_arch, true, true, @"arch_a")); + ASSERT_TRUE( + seed_cache(root.url, cache_manifest('3'), fake.loader, unrelated)); ASSERT_TRUE(seed_cache(isolated.url, source_dict, fake.loader, other_root)); NSDictionary* isolation = nil; - NSDictionary* unselected_stage = nil; NSDictionary* unrelated_stage = nil; ASSERT_TRUE(asset_tree_snapshot(isolated.url, isolation)); - ASSERT_TRUE(asset_tree_snapshot(unselected.stage, unselected_stage)); ASSERT_TRUE(asset_tree_snapshot(unrelated.stage, unrelated_stage)); FBSyntheticPTE pte( {{"empty", {}}, {"first", - {{"UnregisteredBackend", [@"not JSON" dataUsingEncoding:NSUTF8StringEncoding], true}, + {{"UnregisteredBackend", + [@"not JSON" dataUsingEncoding:NSUTF8StringEncoding], + true}, {"CoreAIBackend", encode(source_dict)}, {"CoreAIBackend", encode(second_dict), true}}}, {"last", @@ -205,15 +249,20 @@ FBSyntheticPTE pte( const int queries = bridge.state().architecture_queries; EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); drain_callbacks(); - EXPECT_EQ(bridge.state().architecture_queries.load(), queries + 1); - EXPECT_EQ(fake.loader->evictions.load(), 3); + EXPECT_EQ(bridge.state().architecture_queries.load(), queries); + EXPECT_EQ(fake.loader->evictions.load(), 4); EXPECT_EQ(fake.loader->specializations.load(), 0); EXPECT_EQ(fake.loader->bindings.load(), 0); EXPECT_EQ(fake.loader->restores.load(), 0); NSArray* ordered = - [@[ source.key, second.key, aot.key ] sortedArrayUsingSelector:@selector(compare:)]; - NSDictionary* bookmarks = - @{source.key : source.bookmark, second.key : second.bookmark, aot.key : aot.bookmark}; + [@[ source.key, second.key, aot.key, other_arch.key ] + sortedArrayUsingSelector:@selector(compare:)]; + NSDictionary* bookmarks = @{ + source.key : source.bookmark, + second.key : second.bookmark, + aot.key : aot.bookmark, + other_arch.key : other_arch.bookmark + }; ASSERT_EQ(evictions.count, ordered.count); for (NSUInteger i = 0; i < ordered.count; ++i) { SCOPED_TRACE(::testing::Message() << "sorted eviction index " << i); @@ -223,31 +272,37 @@ FBSyntheticPTE pte( cache_entry_removed(root.url, second); cache_entry_removed(root.url, aot); EXPECT_NE(fake.loader.models[other_root.bookmark], nil); - EXPECT_NE(fake.loader.models[unselected.bookmark], nil); + EXPECT_EQ(fake.loader.models[other_arch.bookmark], nil); + cache_entry_removed(root.url, other_arch); EXPECT_TRUE(snapshot_matches(isolated.url, isolation)); - EXPECT_TRUE(snapshot_matches(unselected.stage, unselected_stage)); EXPECT_TRUE(snapshot_matches(unrelated.stage, unrelated_stage)); EXPECT_EQ(pte.backend_indices, (std::vector{1, 2, 3})); - EXPECT_EQ(pte.backend_descriptors, - (std::vector{"CoreAIBackend", "CoreAIBackend", "CoreAIBackend"})); + EXPECT_EQ( + pte.backend_descriptors, + (std::vector{ + "CoreAIBackend", "CoreAIBackend", "CoreAIBackend"})); EXPECT_TRUE(pte.check_released()); } } TEST_F(CoreAICacheTest, FilePathOverloadClearsReferencedEntry) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; BookmarkLoaderScope fake; TestDirectory distribution; ASSERT_NE(distribution.url, nil); CacheEntry entry; ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, entry)); - FBSyntheticPTE pte({{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); + FBSyntheticPTE pte( + {{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); NSURL* path = [distribution.url URLByAppendingPathComponent:@"model.pte"]; - ASSERT_TRUE([[NSData dataWithBytes:pte.bytes.data() length:pte.bytes.size()] writeToURL:path - options:0 - error:nil]); - EXPECT_EQ(clear_cache_for_pte(path.path.UTF8String, root.url.path.UTF8String), Error::Ok); + ASSERT_TRUE([[NSData dataWithBytes:pte.bytes.data() + length:pte.bytes.size()] writeToURL:path + options:0 + error:nil]); + EXPECT_EQ( + clear_cache_for_pte(path.path.UTF8String, root.url.path.UTF8String), + Error::Ok); drain_callbacks(); EXPECT_EQ(fake.loader->evictions.load(), 1); cache_entry_removed(root.url, entry); @@ -256,37 +311,22 @@ FBSyntheticPTE pte( TEST_F(CoreAICacheTest, RejectsInvalidLaterDelegatesBeforeMutation) { @autoreleasepool { - const char* rows[] = { - "malformed manifest", - "wrong AOT platform", - "backend load failure", - "newer OS", - "missing architecture"}; - for (int row = 0; row < 5; ++row) { + const char* rows[] = {"malformed manifest", "backend load failure"}; + for (int row = 0; row < 2; ++row) { SCOPED_TRACE(rows[row]); - BookmarkDirectory root; + TestDirectory root; BookmarkLoaderScope fake; CacheEntry source; ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, source)); NSDictionary* snapshot = nil; ASSERT_TRUE(asset_tree_snapshot(root.url, snapshot)); PteDelegateSpec later{"CoreAIBackend", encode(aot_manifest_dict()), true}; - if (row == 0) later.data = [@"malformed" dataUsingEncoding:NSUTF8StringEncoding]; - if (row == 1) { - auto dict = aot_manifest_dict(); - dict[@"platform"] = @"iOS"; - later.data = encode(dict); - } - if (row == 3) { - auto dict = aot_manifest_dict(); - dict[@"min_deployment_version"] = @"999999.0"; - later.data = encode(dict); - } - if (row == 4) - bridge.state().device_architecture = @"missing_arch"; + if (row == 0) + later.data = [@"malformed" dataUsingEncoding:NSUTF8StringEncoding]; FBSyntheticPTE pte( - {{"first", {{"CoreAIBackend", encode(manifest_dict())}}}, {"later", {later}}}); - if (row == 2) { + {{"first", {{"CoreAIBackend", encode(manifest_dict())}}}, + {"later", {later}}}); + if (row == 1) { pte.fault_call = 2; pte.load_fault = FBSyntheticPTE::LoadFault::Error; pte.load_error = Error::AccessFailed; @@ -295,9 +335,8 @@ FBSyntheticPTE pte( const auto error = clear_cache_for_pte(pte, root.url.path.UTF8String); drain_callbacks(); EXPECT_NE(error, Error::Ok); - if (row == 2) EXPECT_EQ(error, Error::AccessFailed); - if (row == 1 || row >= 3) - EXPECT_EQ(error, Error::DelegateInvalidCompatibility); + if (row == 1) + EXPECT_EQ(error, Error::AccessFailed); EXPECT_EQ(fake.loader->evictions.load(), 0); EXPECT_EQ(fake.loader->specializations.load(), 0); EXPECT_EQ(fake.loader->bindings.load(), 0); @@ -309,15 +348,181 @@ FBSyntheticPTE pte( } } +TEST_F(CoreAICacheTest, ClearsWarmVariantsWithoutHostCompatibility) { + @autoreleasepool { + TestDirectory root; + BookmarkLoaderScope fake; + auto source = manifest_dict(); + auto aot = aot_manifest_dict(); + aot[@"platform"] = @"iOS"; + CacheEntry mac, ios, compiled; + ASSERT_TRUE(seed_cache( + root.url, source, fake.loader, mac, true, false, @"unknown_mac_arch")); + ASSERT_TRUE(seed_cache( + root.url, + source, + fake.loader, + ios, + true, + false, + @"unknown_ios_arch", + @"iOS")); + ASSERT_TRUE(seed_cache( + root.url, aot, fake.loader, compiled, true, false, @"arch_a", @"iOS")); + source[@"min_deployment_version"] = @"999999.0"; + aot[@"min_deployment_version"] = @"999999.0"; + bridge.state().device_architecture = nil; + const int queries = bridge.state().architecture_queries; + FBSyntheticPTE pte( + {{"forward", + {{"CoreAIBackend", encode(source)}, + {"CoreAIBackend", encode(aot), true}}}}); + EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); + EXPECT_EQ(fake.loader->evictions.load(), 3); + EXPECT_EQ(bridge.state().architecture_queries.load(), queries); + cache_entry_removed(root.url, mac); + cache_entry_removed(root.url, ios); + cache_entry_removed(root.url, compiled); + } +} + +TEST_F(CoreAICacheTest, ClearsLegacyAotWithoutAttribution) { + @autoreleasepool { + TestDirectory root; + BookmarkLoaderScope fake; + CacheEntry first, second; + auto dict = aot_manifest_dict(); + ASSERT_TRUE(seed_cache( + root.url, + dict, + fake.loader, + first, + true, + false, + @"arch_a", + @"macOS", + false)); + ASSERT_TRUE(seed_cache( + root.url, + dict, + fake.loader, + second, + true, + false, + @"arch_b", + @"macOS", + false)); + FBSyntheticPTE pte({{"forward", {{"CoreAIBackend", encode(dict)}}}}); + EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); + EXPECT_EQ(fake.loader->evictions.load(), 2); + cache_entry_removed(root.url, first); + cache_entry_removed(root.url, second); + } +} + +TEST_F( + CoreAICacheTest, + UnattributedEntriesReportIncompleteButClearKnownMatches) { + for (bool corrupt : {false, true}) { + SCOPED_TRACE(corrupt); + @autoreleasepool { + TestDirectory root; + BookmarkLoaderScope fake; + CacheEntry unknown, known; + ASSERT_TRUE(seed_cache( + root.url, + manifest_dict(), + fake.loader, + unknown, + true, + false, + @"old_arch", + @"macOS", + corrupt)); + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, known)); + if (corrupt) { + ASSERT_TRUE([[@"{}" dataUsingEncoding:NSUTF8StringEncoding] + writeToURL:cache_identity_url(root.url, unknown.key) + options:0 + error:nil]); + } + FBSyntheticPTE pte( + {{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); + EXPECT_EQ( + clear_cache_for_pte(pte, root.url.path.UTF8String), + Error::InvalidExternalData); + EXPECT_EQ(fake.loader->evictions.load(), 1); + cache_entry_removed(root.url, known); + EXPECT_TRUE(cache_exists(test_bookmark_url(root.url, unknown.key))); + EXPECT_NE(fake.loader.models[unknown.bookmark], nil); + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); + cache_entry_removed(root.url, unknown); + } + } +} + +TEST_F(CoreAICacheTest, ClearsAttributionOnlyAndStagingEntriesWithoutSdk) { + @autoreleasepool { + TestDirectory root; + BookmarkLoaderScope fake; + CacheEntry metadata_only, staged; + ASSERT_TRUE(seed_cache( + root.url, + manifest_dict(), + fake.loader, + metadata_only, + false, + false, + @"other_arch")); + ASSERT_TRUE(seed_cache( + root.url, manifest_dict(), fake.loader, staged, false, true)); + bridge.state().available = false; + FBSyntheticPTE pte( + {{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); + EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); + EXPECT_EQ(fake.loader->evictions.load(), 0); + cache_entry_removed(root.url, metadata_only); + cache_entry_removed(root.url, staged); + } +} + +TEST_F(CoreAICacheTest, MaintenanceUnlinksStagingRootWithoutFollowingIt) { + @autoreleasepool { + TestDirectory root, outside; + BookmarkLoaderScope fake; + CacheEntry entry; + ASSERT_TRUE( + seed_cache(root.url, manifest_dict(), fake.loader, entry, true, false)); + NSURL* sentinel = [outside.url URLByAppendingPathComponent:@"sentinel"]; + NSData* contents = [@"untouched" dataUsingEncoding:NSUTF8StringEncoding]; + ASSERT_TRUE([contents writeToURL:sentinel options:0 error:nil]); + ASSERT_TRUE([NSFileManager.defaultManager + createDirectoryAtURL:[entry.stage URLByDeletingLastPathComponent] + withIntermediateDirectories:NO + attributes:nil + error:nil]); + ASSERT_EQ( + symlink( + outside.url.fileSystemRepresentation, + entry.stage.fileSystemRepresentation), + 0); + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); + EXPECT_TRUE([[NSData dataWithContentsOfURL:sentinel] isEqual:contents]); + EXPECT_EQ(fake.loader->evictions.load(), 1); + cache_entry_removed(root.url, entry); + } +} + TEST_F(CoreAICacheTest, ProcessedBuffersRemainAliveThroughEviction) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; CacheEntry source; NSData* manifest = encode(manifest_dict()); - FBSyntheticPTE valid({{"forward", - {{"CoreAIBackend", manifest, true}, - {"CoreAIBackend", manifest, true}, - {"CoreAIBackend", manifest}}}}); + FBSyntheticPTE valid( + {{"forward", + {{"CoreAIBackend", manifest, true}, + {"CoreAIBackend", manifest, true}, + {"CoreAIBackend", manifest}}}}); // Destroy the loader scope before the PTE captured by its callback. BookmarkLoaderScope fake; ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, source)); @@ -327,7 +532,8 @@ FBSyntheticPTE valid({{"forward", FBSyntheticPTE* observed = &valid; fake.loader.onEvict = ^(NSData*) { ++observations; - program_alive = observed->program_loads.load() == observed->program_releases.load() + 1; + program_alive = observed->program_loads.load() == + observed->program_releases.load() + 1; backend_releases = observed->backend_releases.load(); }; EXPECT_EQ(clear_cache_for_pte(valid, root.url.path.UTF8String), Error::Ok); @@ -344,32 +550,45 @@ FBSyntheticPTE valid({{"forward", } } -TEST_F(CoreAICacheTest, InventoriesBookmarkAndStagingUnionPreservingUnownedFiles) { +TEST_F( + CoreAICacheTest, + InventoriesBookmarkAndStagingUnionPreservingUnownedFiles) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; BookmarkLoaderScope fake; CacheEntry both, bookmark_only, stage_only; ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, both)); - ASSERT_TRUE(seed_cache(root.url, cache_manifest('2'), fake.loader, bookmark_only, true, false)); - ASSERT_TRUE(seed_cache(root.url, cache_manifest('3'), fake.loader, stage_only, false, true)); - NSURL* temporary = [root.url URLByAppendingPathComponent:@"staging/.staging-unkeyed"]; + ASSERT_TRUE(seed_cache( + root.url, + cache_manifest('2'), + fake.loader, + bookmark_only, + true, + false)); + ASSERT_TRUE(seed_cache( + root.url, cache_manifest('3'), fake.loader, stage_only, false, true)); + NSURL* temporary = + [root.url URLByAppendingPathComponent:@"staging/.staging-unkeyed"]; ASSERT_TRUE([NSFileManager.defaultManager createDirectoryAtURL:temporary withIntermediateDirectories:NO attributes:nil error:nil]); NSURL* untouched = [temporary URLByAppendingPathComponent:@"partial"]; - ASSERT_TRUE([[@"do not infer ownership" dataUsingEncoding:NSUTF8StringEncoding] - writeToURL:untouched + ASSERT_TRUE([[@"do not infer ownership" + dataUsingEncoding:NSUTF8StringEncoding] writeToURL:untouched + options:0 + error:nil]); + NSURL* external_sentinel = + [root.url URLByAppendingPathComponent:@"caller.pte"]; + ASSERT_TRUE([[@"PTE" dataUsingEncoding:NSUTF8StringEncoding] + writeToURL:external_sentinel options:0 error:nil]); - NSURL* external_sentinel = [root.url URLByAppendingPathComponent:@"caller.pte"]; - ASSERT_TRUE([[@"PTE" dataUsingEncoding:NSUTF8StringEncoding] writeToURL:external_sentinel - options:0 - error:nil]); NSDictionary* temp_snapshot = nil; NSDictionary* locks = nil; ASSERT_TRUE(asset_tree_snapshot(temporary, temp_snapshot)); - ASSERT_TRUE(asset_tree_snapshot([root.url URLByAppendingPathComponent:@"locks"], locks)); + ASSERT_TRUE(asset_tree_snapshot( + [root.url URLByAppendingPathComponent:@"locks"], locks)); const int queries = bridge.state().architecture_queries; EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); EXPECT_EQ(fake.loader->evictions.load(), 2); @@ -379,7 +598,8 @@ FBSyntheticPTE valid({{"forward", cache_entry_removed(root.url, both); cache_entry_removed(root.url, bookmark_only); cache_entry_removed(root.url, stage_only); - EXPECT_TRUE(snapshot_matches([root.url URLByAppendingPathComponent:@"locks"], locks)); + EXPECT_TRUE(snapshot_matches( + [root.url URLByAppendingPathComponent:@"locks"], locks)); EXPECT_TRUE(snapshot_matches(temporary, temp_snapshot)); EXPECT_TRUE(cache_exists(external_sentinel)); EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); @@ -389,10 +609,11 @@ FBSyntheticPTE valid({{"forward", TEST_F(CoreAICacheTest, RemovesStagingOnlyEntriesWithoutSdkAvailability) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; BookmarkLoaderScope fake; CacheEntry stage_only; - ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, stage_only, false, true)); + ASSERT_TRUE(seed_cache( + root.url, manifest_dict(), fake.loader, stage_only, false, true)); bridge.state().available = false; EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); cache_entry_removed(root.url, stage_only); @@ -401,11 +622,131 @@ FBSyntheticPTE valid({{"forward", } } +TEST_F(CoreAICacheTest, WarnsAndContinuesPastMissingOrCorruptBookmarks) { + for (bool pte_clear : {false, true}) { + for (int corruption = 0; corruption < 6; ++corruption) { + SCOPED_TRACE(pte_clear ? "PTE" : "root"); + SCOPED_TRACE(corruption); + @autoreleasepool { + TestDirectory root, outside; + NSURL* sentinel = [outside.url URLByAppendingPathComponent:@"sentinel"]; + NSData* contents = + [@"untouched" dataUsingEncoding:NSUTF8StringEncoding]; + ASSERT_TRUE([contents writeToURL:sentinel options:0 error:nil]); + BookmarkLoaderScope fake; + CacheEntry damaged, healthy; + ASSERT_TRUE( + seed_cache(root.url, manifest_dict(), fake.loader, damaged)); + ASSERT_TRUE( + seed_cache(root.url, cache_manifest('2'), fake.loader, healthy)); + if ([damaged.key compare:healthy.key] == NSOrderedDescending) { + std::swap(damaged, healthy); + } + NSURL* path = test_bookmark_url(root.url, damaged.key); + if (corruption == 0 || corruption >= 3) { + ASSERT_EQ(unlink(path.fileSystemRepresentation), 0); + if (corruption == 3) { + ASSERT_EQ(mkfifo(path.fileSystemRepresentation, 0600), 0); + } else if (corruption == 4) { + ASSERT_EQ( + symlink( + sentinel.fileSystemRepresentation, + path.fileSystemRepresentation), + 0); + } else if (corruption == 5) { + ASSERT_EQ(mkdir(path.fileSystemRepresentation, 0700), 0); + ASSERT_TRUE([contents + writeToURL:[path URLByAppendingPathComponent:@"invalid"] + options:0 + error:nil]); + } + } else { + FileDescriptor file( + open(path.fileSystemRepresentation, O_WRONLY | O_NOFOLLOW)); + ASSERT_GE(file.get(), 0); + ASSERT_EQ( + ftruncate(file.get(), corruption == 1 ? 0 : 8 * 1024 * 1024 + 1), + 0); + } + FBSyntheticPTE pte( + {{"forward", + {{"CoreAIBackend", encode(manifest_dict())}, + {"CoreAIBackend", encode(cache_manifest('2')), true}}}}); + auto clear = [&] { + return pte_clear ? clear_cache_for_pte(pte, root.url.path.UTF8String) + : clear_cache(root.url.path.UTF8String); + }; + ::testing::internal::CaptureStderr(); + const auto error = clear(); + const std::string logs = ::testing::internal::GetCapturedStderr(); + EXPECT_EQ(error, Error::Ok); + if constexpr ( + ET_LOG_ENABLED && + runtime::LogLevel::Info >= runtime::LogLevel::ET_MIN_LOG_LEVEL) { + EXPECT_NE(logs.find(damaged.key.UTF8String), std::string::npos); + EXPECT_NE( + logs.find( + corruption == 0 ? "has a missing bookmark" + : "has a corrupt bookmark"), + std::string::npos); + EXPECT_NE( + logs.find("SDK eviction could not be confirmed"), + std::string::npos); + EXPECT_EQ( + logs.find(std::string( + static_cast(damaged.bookmark.bytes), + damaged.bookmark.length)), + std::string::npos); + } + cache_entry_removed(root.url, damaged); + cache_entry_removed(root.url, healthy); + EXPECT_TRUE([[NSData dataWithContentsOfURL:sentinel] isEqual:contents]); + EXPECT_EQ(fake.loader->evictions.load(), 1); + EXPECT_EQ(fake.loader->restores.load(), 0); + EXPECT_NE(fake.loader.models[damaged.bookmark], nil); + EXPECT_EQ(fake.loader.models[healthy.bookmark], nil); + ::testing::internal::CaptureStderr(); + const auto repeated = clear(); + const std::string repeated_logs = + ::testing::internal::GetCapturedStderr(); + EXPECT_EQ(repeated, Error::Ok); + EXPECT_TRUE(repeated_logs.empty()); + EXPECT_EQ(fake.loader->evictions.load(), 1); + EXPECT_TRUE(pte.check_released()); + } + } + } +} + +TEST_F(CoreAICacheTest, CorruptBookmarkCleanupDoesNotRequireSdkAndCanRetry) { + @autoreleasepool { + TestDirectory root; + BookmarkLoaderScope fake; + CacheEntry damaged; + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, damaged)); + NSURL* path = test_bookmark_url(root.url, damaged.key); + ASSERT_TRUE([NSData.data writeToURL:path options:0 error:nil]); + bridge.state().available = false; + { + StorageFaultScope fault(StorageOperation::Remove); + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::AccessFailed); + EXPECT_EQ(fault.hits, 1); + EXPECT_TRUE(cache_exists(path)); + EXPECT_TRUE(cache_exists(damaged.stage)); + } + EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); + cache_entry_removed(root.url, damaged); + EXPECT_EQ(fake.loader->evictions.load(), 0); + EXPECT_EQ(fake.loader->restores.load(), 0); + EXPECT_NE(fake.loader.models[damaged.bookmark], nil); + } +} + TEST_F(CoreAICacheTest, NilLoaderFactoryPreservesEntriesAndAllowsRetry) { @autoreleasepool { ASSERT_TRUE(bridge.state().available); ASSERT_FALSE(bridge.state().fail_loader_factory); - BookmarkDirectory root; + TestDirectory root; BookmarkLoaderScope fake; CacheEntry entry; ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, entry)); @@ -427,7 +768,7 @@ FBSyntheticPTE valid({{"forward", TEST_F(CoreAICacheTest, UnavailableSdkPreservesTrackedEntriesAndAllowsRetry) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; BookmarkLoaderScope fake; CacheEntry entry; ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, entry)); @@ -449,13 +790,15 @@ FBSyntheticPTE valid({{"forward", int match_index; bool bookmark_removal; const char* name; - } rows[] = {{2, false, "staged-tree removal"}, {6, true, "bookmark removal"}}; + } rows[] = { + {2, false, "staged-tree removal"}, {6, true, "bookmark removal"}}; for (const auto& row : rows) { SCOPED_TRACE(row.name); - BookmarkDirectory root; + TestDirectory root; BookmarkLoaderScope fake; CacheEntry entry; - ASSERT_TRUE(seed_cache(root.url, aot_manifest_dict(), fake.loader, entry)); + ASSERT_TRUE( + seed_cache(root.url, aot_manifest_dict(), fake.loader, entry)); { StorageFaultScope fault(StorageOperation::Remove); fault.match_index = row.match_index; @@ -478,25 +821,55 @@ FBSyntheticPTE valid({{"forward", } } +TEST_F(CoreAICacheTest, AttributionRemovalFailureLeavesDiscoverableRetry) { + @autoreleasepool { + TestDirectory root; + BookmarkLoaderScope fake; + CacheEntry entry; + ASSERT_TRUE( + seed_cache(root.url, manifest_dict(), fake.loader, entry, true, false)); + FBSyntheticPTE pte( + {{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); + { + StorageFaultScope fault(StorageOperation::Remove); + fault.match_index = 2; + EXPECT_EQ( + clear_cache_for_pte(pte, root.url.path.UTF8String), + Error::AccessFailed); + EXPECT_EQ(fault.hits, 1); + } + EXPECT_FALSE(cache_exists(test_bookmark_url(root.url, entry.key))); + EXPECT_TRUE(cache_exists(cache_identity_url(root.url, entry.key))); + EXPECT_EQ(fake.loader->evictions.load(), 1); + bridge.state().available = false; + EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); + EXPECT_EQ(fake.loader->evictions.load(), 1); + cache_entry_removed(root.url, entry); + } +} + TEST_F(CoreAICacheTest, IndependentKeysContinueInSortedOrderAfterFailures) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; BookmarkLoaderScope fake; CacheEntry first, second, third; ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, first)); ASSERT_TRUE(seed_cache(root.url, cache_manifest('2'), fake.loader, second)); ASSERT_TRUE(seed_cache(root.url, cache_manifest('3'), fake.loader, third)); - NSArray* ordered = - [@[ first.key, second.key, third.key ] sortedArrayUsingSelector:@selector(compare:)]; - NSDictionary* bookmarks = - @{first.key : first.bookmark, second.key : second.bookmark, third.key : third.bookmark}; + NSArray* ordered = [@[ first.key, second.key, third.key ] + sortedArrayUsingSelector:@selector(compare:)]; + NSDictionary* bookmarks = @{ + first.key : first.bookmark, + second.key : second.bookmark, + third.key : third.bookmark + }; NSMutableArray* evicted = [NSMutableArray array]; BookmarkFakeLoader* loader = fake.loader; fake.loader.onEvict = ^(NSData* bookmark) { [evicted addObject:bookmark]; loader.evictError = [bookmark isEqual:bookmarks[ordered[1]]] - ? [NSError errorWithDomain:NSPOSIXErrorDomain code:EBUSY userInfo:nil] - : nil; + ? [NSError errorWithDomain:NSPOSIXErrorDomain code:EBUSY userInfo:nil] + : nil; }; StorageFaultScope fault(StorageOperation::BeforeEvict); EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::AccessFailed); @@ -511,5 +884,5 @@ FBSyntheticPTE valid({{"forward", } } -} // namespace -} // namespace executorch::backends::coreai::testing +} // namespace +} // namespace executorch::backends::coreai::testing diff --git a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm index 9cf065e9fc8..c14447b31c2 100644 --- a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm @@ -26,30 +26,46 @@ } // These cases never retain a delegate handle beyond initialization. -Error initialize_and_destroy(NSDictionary* dict, const NamedDataMap* data, NSString* root) { - if (root == nil) return Error::InvalidArgument; +Error initialize_and_destroy( + NSDictionary* dict, + const NamedDataMap* data, + NSString* root) { + if (root == nil) + return Error::InvalidArgument; BackendOptions<1> options; auto error = options.set_option("coreai_assets_dir", root.UTF8String); - if (error != Error::Ok) return error; + if (error != Error::Ok) + return error; auto* backend = get_backend_class("CoreAIBackend"); - if (backend == nullptr) return Error::Internal; - BackendInitContext context(nullptr, nullptr, "forward", data, - {options.view().data(), options.view().size()}); + if (backend == nullptr) + return Error::Internal; + BackendInitContext context( + nullptr, + nullptr, + "forward", + data, + {options.view().data(), options.view().size()}); NSData* json = encode(dict); - if (json == nil) return Error::InvalidArgument; + if (json == nil) + return Error::InvalidArgument; FreeableBuffer processed(json.bytes, json.length, nullptr); auto handle = backend->init(context, &processed, {}); - if (!handle.ok()) return handle.error(); + if (!handle.ok()) + return handle.error(); backend->destroy(handle.get()); return Error::Ok; } -dispatch_time_t deadline() { return dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC); } +dispatch_time_t deadline() { + return dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC); +} class CoreAIEvictionTest : public ::testing::Test { protected: ScopedFakeBridgeState bridge; - void SetUp() override { runtime_init(); } + void SetUp() override { + runtime_init(); + } }; // Release the callback before waiting; never unwind storage borrowed by a hang. @@ -75,20 +91,27 @@ void release() { } void join() { release(); - if (joined) return; + if (joined) + return; const auto completion_deadline = deadline(); if (eviction_started) - eviction_wait = dispatch_semaphore_wait(eviction_done, completion_deadline); - if (loading_started) loading_wait = dispatch_semaphore_wait(loading_done, completion_deadline); - if ((eviction_started && eviction_wait != 0) || (loading_started && loading_wait != 0)) { - ADD_FAILURE() << "Eviction/load workers did not return after releasing the gate"; + eviction_wait = + dispatch_semaphore_wait(eviction_done, completion_deadline); + if (loading_started) + loading_wait = dispatch_semaphore_wait(loading_done, completion_deadline); + if ((eviction_started && eviction_wait != 0) || + (loading_started && loading_wait != 0)) { + ADD_FAILURE() + << "Eviction/load workers did not return after releasing the gate"; std::_Exit(EXIT_FAILURE); } - const int eviction_join = eviction_started ? pthread_join(eviction, nullptr) : 0; - const int loading_join = loading_started ? pthread_join(loading, nullptr) : 0; + const int eviction_join = + eviction_started ? pthread_join(eviction, nullptr) : 0; + const int loading_join = + loading_started ? pthread_join(loading, nullptr) : 0; if (eviction_join != 0 || loading_join != 0) { - ADD_FAILURE() << "Cannot join eviction/load workers: " << eviction_join << ", " - << loading_join; + ADD_FAILURE() << "Cannot join eviction/load workers: " << eviction_join + << ", " << loading_join; std::_Exit(EXIT_FAILURE); } auto drained = bridge.wait_for_callbacks(deadline()); @@ -98,9 +121,11 @@ void join() { } joined = true; } - ~ReleaseAndJoin() { join(); } + ~ReleaseAndJoin() { + join(); + } }; -} // namespace +} // namespace @interface PinBookmarkLoader : BookmarkFakeLoader @property(nonatomic, weak) BookmarkFakeModel* temporary; @@ -108,13 +133,15 @@ @interface PinBookmarkLoader : BookmarkFakeLoader @end @implementation PinBookmarkLoader -- (void)specializeModelAtURL:(NSURL*)url completion:(ETCoreAIAcquisitionCompletion)completion { +- (void)specializeModelAtURL:(NSURL*)url + completion:(ETCoreAIAcquisitionCompletion)completion { ++specializations; self.retainedCompletion = completion; @autoreleasepool { BookmarkFakeModel* model = [[BookmarkFakeModel alloc] init]; model.owner = self; - model.bookmark = [@"pinned-bookmark" dataUsingEncoding:NSUTF8StringEncoding]; + model.bookmark = + [@"pinned-bookmark" dataUsingEncoding:NSUTF8StringEncoding]; self.temporary = model; completion(ETCoreAIAcquisitionStatusHit, model, nil); model = nil; @@ -126,12 +153,16 @@ - (void)restoreModelFromBookmark:(NSData*)bookmark self.retainedCompletion = completion; @autoreleasepool { BookmarkFakeModel* model = self.temporary; - completion(model == nil ? ETCoreAIAcquisitionStatusMiss : ETCoreAIAcquisitionStatusHit, model, - nil); + completion( + model == nil ? ETCoreAIAcquisitionStatusMiss + : ETCoreAIAcquisitionStatusHit, + model, + nil); model = nil; } } -- (void)evictModelWithBookmark:(NSData*)bookmark completion:(void (^)(NSError*))completion { +- (void)evictModelWithBookmark:(NSData*)bookmark + completion:(void (^)(NSError*))completion { ++evictions; bool busy; @autoreleasepool { @@ -148,7 +179,7 @@ - (void)clearRetainedState { namespace { TEST_F(CoreAIEvictionTest, ConfirmedMissRecoveryEvictsOnlyCurrentBookmark) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; ASSERT_NE(root.url, nil); BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); @@ -167,7 +198,8 @@ - (void)clearRetainedState { NSString* source_path = fake.loader.lastSpecializeURL; ASSERT_NE(source_path, nil); NSURL* source_url = [NSURL fileURLWithPath:source_path]; - EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + EXPECT_FALSE( + [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); fake.loader.restoreMiss = YES; fake.loader.specializeError = bookmark_test_error(); @@ -175,7 +207,8 @@ - (void)clearRetainedState { auto after_failure = saved_bookmark(root.url, key); ASSERT_TRUE(after_failure.ok()); EXPECT_TRUE([after_failure.get() isEqual:original]); - EXPECT_TRUE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + EXPECT_TRUE( + [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); fake.loader.specializeError = nil; ASSERT_EQ(initialize_and_destroy(dict, &data, root.url.path), Error::Ok); EXPECT_EQ(fake.loader->specializations.load(), 3); @@ -186,12 +219,14 @@ - (void)clearRetainedState { NSData* saved = saved_result.get(); ASSERT_NE(saved, nil); EXPECT_FALSE([saved isEqual:original]); - EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + EXPECT_FALSE( + [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); ASSERT_EQ( clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); EXPECT_NE(fake.loader.models[original], nil); EXPECT_EQ(fake.loader.models[saved], nil); - EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + EXPECT_FALSE( + [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); fake.loader.restoreMiss = NO; fake.loader.emptyBookmark = YES; EXPECT_NE(initialize_and_destroy(dict, &data, root.url.path), Error::Ok); @@ -205,9 +240,11 @@ - (void)clearRetainedState { } } -TEST_F(CoreAIEvictionTest, BusyAndUnknownEvictionRetainBookmarkUntilConfirmedAbsent) { +TEST_F( + CoreAIEvictionTest, + BusyAndUnknownEvictionRetainBookmarkUntilConfirmedAbsent) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; ASSERT_NE(root.url, nil); BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); @@ -225,20 +262,21 @@ - (void)clearRetainedState { StorageFaultScope fault(StorageOperation::Remove); ASSERT_EQ( initialize_and_destroy(manifest_dict(), &data, root.url.path), - Error::AccessFailed); + Error::Ok); EXPECT_EQ(fault.hits, 1); } auto saved_result = saved_bookmark(root.url, key); ASSERT_TRUE(saved_result.ok()); NSData* saved = saved_result.get(); ASSERT_NE(saved, nil); - NSURL* source_url = - [[[root.url URLByAppendingPathComponent:@"staging"] URLByAppendingPathComponent:key] - URLByAppendingPathComponent:selected->path.lastPathComponent]; + NSURL* source_url = [[[root.url URLByAppendingPathComponent:@"staging"] + URLByAppendingPathComponent:key] + URLByAppendingPathComponent:selected->path.lastPathComponent]; EXPECT_TRUE( [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); for (bool probe_error : {false, true}) { - SCOPED_TRACE(probe_error ? "unknown restore failure" : "restore hit/busy"); + SCOPED_TRACE( + probe_error ? "unknown restore failure" : "restore hit/busy"); fake.loader.evictError = bookmark_test_error(EBUSY); fake.loader.restoreError = probe_error ? bookmark_test_error() : nil; EXPECT_NE( @@ -266,9 +304,12 @@ - (void)clearRetainedState { auto absent = saved_bookmark(root.url, key); ASSERT_TRUE(absent.ok()); EXPECT_EQ(absent.get(), nil); - EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + EXPECT_FALSE( + [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); fake.loader.evictError = nil; - ASSERT_EQ(initialize_and_destroy(manifest_dict(), &data, root.url.path), Error::Ok); + ASSERT_EQ( + initialize_and_destroy(manifest_dict(), &data, root.url.path), + Error::Ok); { StorageFaultScope fault(StorageOperation::Remove); EXPECT_NE( @@ -283,14 +324,17 @@ - (void)clearRetainedState { auto removed = saved_bookmark(root.url, key); ASSERT_TRUE(removed.ok()); EXPECT_EQ(removed.get(), nil); - EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + EXPECT_FALSE( + [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); EXPECT_EQ(data.requests.load(), data.releases.load()); } } -TEST_F(CoreAIEvictionTest, PreparedPinsBlockEvictionWithoutRetainedCompletionLeak) { +TEST_F( + CoreAIEvictionTest, + PreparedPinsBlockEvictionWithoutRetainedCompletionLeak) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; ASSERT_NE(root.url, nil); TestData data; auto selected = bookmark_manifest(); @@ -304,8 +348,8 @@ - (void)clearRetainedState { // failures. bridge.state().bookmark_loader = loader; @autoreleasepool { - auto pinned = - acquire_bookmark_model(selected.get(), &data, root.url.path, @"macOS", @"arch_b", loader); + auto pinned = acquire_bookmark_model( + selected.get(), &data, root.url.path, @"macOS", @"arch_b", loader); ASSERT_TRUE(pinned.ok()); EXPECT_NE(loader.temporary, nil); EXPECT_NE(clear_bookmark_assets(root.url.path, key, loader), Error::Ok); @@ -320,10 +364,11 @@ - (void)clearRetainedState { ASSERT_TRUE(removed.ok()); EXPECT_EQ(removed.get(), nil); EXPECT_EQ(loader->evictions.load(), 2); - NSURL* source_url = - [[[root.url URLByAppendingPathComponent:@"staging"] URLByAppendingPathComponent:key] - URLByAppendingPathComponent:selected->path.lastPathComponent]; - EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_url.path]); + NSURL* source_url = [[[root.url URLByAppendingPathComponent:@"staging"] + URLByAppendingPathComponent:key] + URLByAppendingPathComponent:selected->path.lastPathComponent]; + EXPECT_FALSE( + [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); [loader clearRetainedState]; EXPECT_EQ(data.requests.load(), data.releases.load()); } @@ -331,7 +376,7 @@ - (void)clearRetainedState { TEST_F(CoreAIEvictionTest, LoadContendsOnActualKeyLockUntilEvictionFinishes) { @autoreleasepool { - BookmarkDirectory root; + TestDirectory root; ASSERT_NE(root.url, nil); BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); @@ -341,7 +386,9 @@ - (void)clearRetainedState { auto key_result = test_bookmark_key(selected.get()); ASSERT_TRUE(key_result.ok()); NSString* key = key_result.get(); - ASSERT_EQ(initialize_and_destroy(manifest_dict(), &data, root.url.path), Error::Ok); + ASSERT_EQ( + initialize_and_destroy(manifest_dict(), &data, root.url.path), + Error::Ok); dispatch_semaphore_t deleting = dispatch_semaphore_create(0); dispatch_semaphore_t proceed = dispatch_semaphore_create(0); dispatch_semaphore_t loading_observed = dispatch_semaphore_create(0); @@ -362,8 +409,13 @@ - (void)clearRetainedState { }; auto load = [&] { @autoreleasepool { - auto acquired = acquire_bookmark_model(selected.get(), &data, root.url.path, @"macOS", - @"arch_b", fake.loader); + auto acquired = acquire_bookmark_model( + selected.get(), + &data, + root.url.path, + @"macOS", + @"arch_b", + fake.loader); load_ok = acquired.ok() && acquired.get() != nil; } dispatch_semaphore_signal(loading_observed); @@ -371,20 +423,23 @@ - (void)clearRetainedState { }; ReleaseAndJoin cleanup{proceed, eviction_done, loading_done, bridge}; const int eviction_start = pthread_create( - &cleanup.eviction, nullptr, + &cleanup.eviction, + nullptr, [](void* context) -> void* { (*static_cast(context))(); return nullptr; }, &evict); cleanup.eviction_started = eviction_start == 0; - const long entered = - cleanup.eviction_started ? dispatch_semaphore_wait(deleting, deadline()) : -1; + const long entered = cleanup.eviction_started + ? dispatch_semaphore_wait(deleting, deadline()) + : -1; int loading_start = -1; long premature_completion = -1; if (entered == 0) { loading_start = pthread_create( - &cleanup.loading, nullptr, + &cleanup.loading, + nullptr, [](void* context) -> void* { (*static_cast(context))(); return nullptr; @@ -393,7 +448,8 @@ - (void)clearRetainedState { cleanup.loading_started = loading_start == 0; if (cleanup.loading_started) { premature_completion = dispatch_semaphore_wait( - loading_observed, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC)); + loading_observed, + dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC)); } } cleanup.join(); @@ -415,4 +471,4 @@ - (void)clearRetainedState { EXPECT_EQ(data.requests.load(), data.releases.load()); } } -} // namespace +} // namespace diff --git a/tools/cmake/preset/default.cmake b/tools/cmake/preset/default.cmake index 7b017cf36b9..cf3a335dd84 100644 --- a/tools/cmake/preset/default.cmake +++ b/tools/cmake/preset/default.cmake @@ -440,6 +440,10 @@ check_required_options_on( EXECUTORCH_BUILD_EXTENSION_LLM ) +check_required_options_on( + IF_ON EXECUTORCH_BUILD_COREAI REQUIRES EXECUTORCH_BUILD_EXTENSION_DATA_LOADER +) + check_required_options_on( IF_ON EXECUTORCH_BUILD_EXTENSION_MODULE REQUIRES EXECUTORCH_BUILD_EXTENSION_DATA_LOADER EXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR From e4f25d617bd623efca14c54e2886af4b2269166d Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Tue, 27 Oct 2026 17:23:26 -0700 Subject: [PATCH 4/7] [CoreAI] Trace cache clearing and verify bounded lock reuse Log eviction request, confirmed SDK absence, and local cleanup completion. Add clear-aware stripe reuse and per-key isolation regressions, and cover durable-save and eviction failure log boundaries. Preserve the previously validated final implementation after placing storage and acquisition changes in their owning PR layers. Test Plan: Exact final CoreAI tree matches the saved validated snapshot: 256 native tests, all five CTest targets, 124 Python tests, and playground rebuild passed. Lower bookmark and acquisition layers were also built and tested independently during placement. --- backends/apple/coreai/README.md | 35 ++++--- .../coreai/runtime/coreai_load_coordinator.mm | 10 +- .../runtime/test/coreai_acquisition_test.mm | 99 +++++++++++++++++++ .../coreai/runtime/test/coreai_cache_test.mm | 4 +- 4 files changed, 128 insertions(+), 20 deletions(-) diff --git a/backends/apple/coreai/README.md b/backends/apple/coreai/README.md index f5d8f6ddd97..419ca3ca656 100644 --- a/backends/apple/coreai/README.md +++ b/backends/apple/coreai/README.md @@ -229,6 +229,24 @@ clearing or after process exit, so waiters and future openers keep using the same inode. Removing the assets root externally requires all loads, sessions and maintenance to stop. +### Lifecycle logging + +Build with `EXECUTORCH_ENABLE_LOGGING=ON` and `EXECUTORCH_LOG_LEVEL=Info` to +observe cache lookup, bookmark restoration hits, missing-bookmark versus +SDK-confirmed misses, specialization start/success, durable bookmark saves, and +cache-clearing milestones. Each message carries the cache key so events from +multiple partitions can be correlated. These messages do not include bookmark +contents, model data, or per-inference logs. + +A cache hit means that the SDK successfully restored a model from its bookmark. +Specialization start means a request to the SDK's persistent default cache; the +SDK does not report whether that request compiled afresh or reused an internal +artifact. The durable-save message confirms local bookmark publication, +including its final directory flush, not independent verification of SDK-private +artifact storage. During clearing, SDK entry absence and successful local +cleanup are reported separately. Failed operations do not emit their success +messages, and clearing an absent entry remains quiet. + ### Clearing cached assets The public C++ maintenance APIs are in `executorch::backends::coreai`: @@ -256,7 +274,7 @@ not load models or materialize assets. Actual SDK eviction still requires a usable bookmark and an available loader. These synchronous calls require callers to unload affected models and prevent -concurrent loads, inference and maintenance across processes. Per-key locks are +concurrent loads, inference and maintenance across processes. Stripe locks are not a whole-root barrier. Clearing first deletes the recorded SDK entry, then staging, then its bookmark. Busy or unknown SDK errors preserve the files; SDK-confirmed absence permits cleanup. Filesystem errors may leave partial @@ -375,21 +393,6 @@ Ninja builds use one architecture per build directory. Current runtime support covers arm64 macOS and iOS device builds. x86_64 builds are blocked by Swift `Float16` availability, and the tested iOS simulator SDKs do not contain Core AI. -### Lifecycle logging - -Build with `EXECUTORCH_ENABLE_LOGGING=ON` and `EXECUTORCH_LOG_LEVEL=Info` to -observe cache lookup, bookmark restoration hits, missing-bookmark versus -SDK-confirmed misses, specialization start/success, and durable bookmark saves. Each message carries the cache key so events from -multiple partitions can be correlated. These messages do not include bookmark -contents, model data, or per-inference logs. - -A cache hit means that the SDK successfully restored a model from its bookmark. -Specialization start means a request to the SDK's persistent default cache; the -SDK does not report whether that request compiled afresh or reused an internal -artifact. The durable-save message confirms local bookmark publication, -including its final directory flush, not independent verification of SDK-private -artifact storage. Failed operations do not emit their success messages. - Enable `EXECUTORCH_BUILD_EXTENSION_DATA_LOADER` for the public PTE-file maintenance API and consumers such as `Module`. Link the CMake target `coreaidelegate`, not just its archive filename. Its diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.mm b/backends/apple/coreai/runtime/coreai_load_coordinator.mm index 4532a159e86..d7efbcd8686 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.mm +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.mm @@ -221,6 +221,7 @@ Error clear_bookmark_assets( return locked.error(); if (locked.get() == nullptr) return Error::Ok; + ET_LOG(Info, "Core AI cache clear started: key=%s", key.UTF8String); auto bookmark = read_bookmark(*locked.get(), /*maintenance=*/true); if (!bookmark.ok() && bookmark.error() != Error::InvalidExternalData) { return bookmark.error(); @@ -246,14 +247,21 @@ Error clear_bookmark_assets( ET_CHECK_OR_RETURN_ERROR( loader != nil, Internal, "Cannot create Core AI model loader"); } + ET_LOG(Info, "Core AI SDK eviction requested: key=%s", key.UTF8String); ET_CHECK_OK_OR_RETURN_ERROR(evict_one(loader, bytes)); + ET_LOG( + Info, + "Core AI SDK cache entry absence confirmed: key=%s", + key.UTF8String); } ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_staging( *locked.get(), true, StorageRemovalMode::Maintenance)); if (!bookmark.ok() || bytes != nil) { ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark(*locked.get())); } - return remove_bookmark_identity(*locked.get()); + ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_identity(*locked.get())); + ET_LOG(Info, "Core AI local cache entry cleared: key=%s", key.UTF8String); + return Error::Ok; } } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm b/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm index d331194a2f3..5734fa49a84 100644 --- a/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm @@ -803,4 +803,103 @@ void expect_order( EXPECT_EQ(logs.find("Core AI cache bookmark saved"), std::string::npos); } +TEST_F(CoreAILifecycleLogTest, FinalBookmarkFlushFailureDoesNotLogDurableSave) { + BookmarkLoaderScope fake; + { + auto locked = lock_bookmark(root.url.path, key); + ASSERT_TRUE(locked.ok()); + ASSERT_EQ( + ensure_bookmark_identity(*locked.get(), manifest, @"macOS", @"arch_b"), + Error::Ok); + } + StorageFaultScope fault(StorageOperation::FullSync); + fault.match_index = 5; + const auto logs = + capture([&] { return acquire(fake.loader, &data); }, Error::AccessFailed); + EXPECT_EQ(fault.hits, 1); + EXPECT_NE(logs.find("Core AI specialization succeeded:"), std::string::npos); + EXPECT_EQ(logs.find("Core AI cache bookmark saved"), std::string::npos); + EXPECT_TRUE([saved() isEqual:fake.loader.lastAcquired.bookmark]); +} + +TEST_F(CoreAILifecycleLogTest, ClearLogsConfirmedAbsenceBeforeLocalCompletion) { + BookmarkLoaderScope fake; + ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); + const auto logs = capture( + [&] { return clear_bookmark_assets(root.url.path, key, fake.loader); }); + expect_order( + logs, + { + "Core AI cache clear started:", + "Core AI SDK eviction requested:", + "Core AI SDK cache entry absence confirmed:", + "Core AI local cache entry cleared:", + }); + EXPECT_EQ(saved(), nil); +} + +TEST_F(CoreAILifecycleLogTest, FailedEvictionWithConfirmedMissLogsAbsence) { + BookmarkLoaderScope fake; + ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); + fake.loader.evictError = [NSError errorWithDomain:NSPOSIXErrorDomain + code:ENOENT + userInfo:nil]; + fake.loader.restoreMiss = YES; + const auto logs = capture( + [&] { return clear_bookmark_assets(root.url.path, key, fake.loader); }); + expect_order( + logs, + { + "Core AI SDK eviction requested:", + "Core AI SDK cache entry absence confirmed:", + "Core AI local cache entry cleared:", + }); +} + +TEST_F(CoreAILifecycleLogTest, EvictionErrorDoesNotLogAbsenceOrCompletion) { + BookmarkLoaderScope fake; + ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); + fake.loader.evictError = [NSError errorWithDomain:NSPOSIXErrorDomain + code:EBUSY + userInfo:nil]; + const auto logs = capture( + [&] { return clear_bookmark_assets(root.url.path, key, fake.loader); }, + Error::Internal); + EXPECT_NE(logs.find("Core AI SDK eviction requested:"), std::string::npos); + EXPECT_EQ( + logs.find("Core AI SDK cache entry absence confirmed:"), + std::string::npos); + EXPECT_EQ(logs.find("Core AI local cache entry cleared:"), std::string::npos); + EXPECT_NE(saved(), nil); +} + +TEST_F( + CoreAILifecycleLogTest, + IdentityRemovalFailureDoesNotLogLocalCompletion) { + BookmarkLoaderScope fake; + ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); + StorageFaultScope fault(StorageOperation::Remove); + fault.match_index = 2; + const auto logs = capture( + [&] { return clear_bookmark_assets(root.url.path, key, fake.loader); }, + Error::AccessFailed); + EXPECT_EQ(fault.hits, 1); + EXPECT_NE( + logs.find("Core AI SDK cache entry absence confirmed:"), + std::string::npos); + EXPECT_EQ(logs.find("Core AI local cache entry cleared:"), std::string::npos); + auto locked = lock_bookmark(root.url.path, key, false); + ASSERT_TRUE(locked.ok()); + ASSERT_NE(locked.get(), nullptr); + auto identity = read_bookmark_identity(*locked.get()); + ASSERT_TRUE(identity.ok()); + EXPECT_NE(identity.get(), nil); +} + +TEST_F(CoreAILifecycleLogTest, AbsentEntryClearRemainsQuiet) { + const auto logs = + capture([&] { return clear_bookmark_assets(root.url.path, key, nil); }); + EXPECT_TRUE(logs.empty()) << logs; +} + } // namespace diff --git a/backends/apple/coreai/runtime/test/coreai_cache_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_test.mm index c9e55500c03..dc323122c0a 100644 --- a/backends/apple/coreai/runtime/test/coreai_cache_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_test.mm @@ -148,9 +148,7 @@ void cache_entry_removed(NSURL* root, const CacheEntry& entry) { EXPECT_FALSE(cache_exists(cache_identity_url(root, entry.key))); EXPECT_FALSE(cache_exists(test_bookmark_url(root, entry.key))); EXPECT_FALSE(cache_exists(entry.stage)); - EXPECT_TRUE(cache_exists([[root URLByAppendingPathComponent:@"locks"] - URLByAppendingPathComponent:[entry.key - stringByAppendingString:@".lock"]])); + EXPECT_TRUE(cache_exists(test_bookmark_lock_url(root, entry.key))); } NSMutableDictionary* cache_manifest(char identity) { From e3c49b5402f44f18ed8c94b8655bcee2648b02c8 Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Thu, 29 Oct 2026 16:26:35 -0700 Subject: [PATCH 5/7] [CoreAI] Keep cache maintenance on validated entry ownership Reuse current cache identity for noncreating inventory and model-prefix selection. Keep SDK eviction and maintenance preflight/deletion under the entry lock, preserve separate strict runtime cleanup, and remove superseded sidecar attribution and migration paths. Update cache tests, documentation and data-loader linkage. Test Plan: Fresh native build and all five scoped CoreAI CTest targets passed. Final incremental build and 5/5 CTests passed after removing duplicate preflight. Scoped lint, explicit formatting and whitespace checks passed. No Python changes or Python suite run. --- backends/apple/coreai/CMakeLists.txt | 7 +- backends/apple/coreai/README.md | 31 ++-- .../apple/coreai/runtime/coreai_bookmarks.h | 22 +++ .../apple/coreai/runtime/coreai_bookmarks.mm | 121 +++++++++++++- backends/apple/coreai/runtime/coreai_cache.h | 10 +- backends/apple/coreai/runtime/coreai_cache.mm | 110 +++++------- .../coreai/runtime/coreai_fault_injection.h | 2 + .../coreai/runtime/coreai_load_coordinator.mm | 49 +++--- .../apple/coreai/runtime/coreai_storage.h | 8 + .../apple/coreai/runtime/coreai_storage.mm | 31 ++++ .../runtime/test/coreai_acquisition_test.mm | 99 ----------- .../coreai/runtime/test/coreai_cache_test.mm | 156 ++++++------------ .../runtime/test/coreai_eviction_test.mm | 37 ++--- .../coreai/runtime/test/coreai_fake_loader.h | 1 + .../coreai/runtime/test/coreai_fake_loader.mm | 2 + 15 files changed, 341 insertions(+), 345 deletions(-) diff --git a/backends/apple/coreai/CMakeLists.txt b/backends/apple/coreai/CMakeLists.txt index a802b8987b6..a7e70f4af16 100644 --- a/backends/apple/coreai/CMakeLists.txt +++ b/backends/apple/coreai/CMakeLists.txt @@ -31,9 +31,11 @@ enable_language(OBJCXX) if(EXECUTORCH_BUILD_SHARED) set(_coreai_library_type SHARED) set(_coreai_runtime executorch_shared) + set(_coreai_data_loader "") else() set(_coreai_library_type STATIC) set(_coreai_runtime executorch) + set(_coreai_data_loader extension_data_loader) endif() set(_coreai_runtime_sources @@ -97,8 +99,9 @@ if(EXECUTORCH_BUILD_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Darwin") target_compile_definitions(coreai_host_test PRIVATE COREAI_ASSETS_TESTING=1) find_library(COREAI_FOUNDATION_FRAMEWORK Foundation REQUIRED) target_link_libraries( - coreai_host_test PRIVATE ${_coreai_runtime} program_schema GTest::gtest - ${COREAI_FOUNDATION_FRAMEWORK} + coreai_host_test + PRIVATE ${_coreai_runtime} ${_coreai_data_loader} program_schema + GTest::gtest ${COREAI_FOUNDATION_FRAMEWORK} ) add_test(NAME coreai_host_test COMMAND coreai_host_test) set_tests_properties(coreai_host_test PROPERTIES TIMEOUT 120) diff --git a/backends/apple/coreai/README.md b/backends/apple/coreai/README.md index 419ca3ca656..22828a5a138 100644 --- a/backends/apple/coreai/README.md +++ b/backends/apple/coreai/README.md @@ -264,14 +264,14 @@ when the directory is omitted or null. A supplied path must be absolute; invalid paths fail without falling back to the default. If loading used a custom root, pass that same root when clearing; the PTE does not store runtime overrides. -Root-wide clearing visits keyed bookmarks, identity records, and staged -directories. PTE clearing finds every Core AI delegate in every method and clears -all recorded variants of its bundles, including other platforms and SDK -architectures. Manifests must be structurally valid before any entry is cleared; -their minimum OS and runtime compatibility do not restrict maintenance. -Discovery requires neither SDK availability nor an architecture query, and does -not load models or materialize assets. Actual SDK eviction still requires a -usable bookmark and an available loader. +Root-wide clearing inventories keyed bookmarks and staged directories. PTE +clearing finds every Core AI delegate in every method and derives each bundle's +model prefix. Matching that validated prefix clears every recorded platform, +device, and specialization variant without querying the host or SDK. All +manifests are validated before any entry is cleared; their minimum OS and runtime +compatibility do not restrict maintenance. Discovery does not load models or +materialize assets. Actual SDK eviction still requires a usable bookmark and an +available loader. These synchronous calls require callers to unload affected models and prevent concurrent loads, inference and maintenance across processes. Stripe locks are @@ -296,15 +296,6 @@ that these APIs cannot target. No history of removed entries is retained. Copied or reused exported artifacts can share keys; clearing them also removes the other copy's warm-cache benefit. Subsequent loads can specialize again. -Identity records are small JSON sidecars beside bookmarks. They retain bundle -identity and cache context, and are checked against their hashed key. Loading -publishes the record before staging or SDK acquisition, and clearing removes it -last. Existing keys and opaque bookmark bytes are unchanged. Older AOT entries -can be found directly from the PTE's architecture list. Older source entries -without attribution cannot always be identified: PTE clearing removes known -matches, logs unresolved keys, and reports incomplete cleanup rather than -removing unrelated entries. A one-time root-wide `clear_cache` handles these -older entries. Missing or corrupt attribution has the same conservative behavior. ### Persistent SDK Rejection @@ -312,9 +303,9 @@ SDK errors are not classified as transient or permanent. Busy errors and all unclassified eviction/restore errors retain the local bookmark for retry; only successful eviction or a confirmed SDK miss permits normal removal. If retries keep failing, first stop every process using the -assets root. Back up or quarantine only the affected key's bookmark, identity -sidecar, and keyed staging outside that root, leaving the stable lock files in -place. A later load can attempt cold acquisition and create new tracking data. +assets root. Back up or quarantine only the affected key's bookmark and keyed +staging outside that root, leaving the stable lock files in place. A later load +can attempt cold acquisition and publish a new bookmark. This manual recovery does not confirm SDK eviction or guarantee disk-space reclamation: keep the backup because it may contain the only remaining eviction handle. Do not perform manual recovery while loading, inference, or maintenance diff --git a/backends/apple/coreai/runtime/coreai_bookmarks.h b/backends/apple/coreai/runtime/coreai_bookmarks.h index bf5440f8500..6646c31803d 100644 --- a/backends/apple/coreai/runtime/coreai_bookmarks.h +++ b/backends/apple/coreai/runtime/coreai_bookmarks.h @@ -19,6 +19,20 @@ namespace executorch::backends::coreai { runtime::Result resolve_bookmark_root( NSString* path, StorageCreationMode mode); +// Missing roots produce an empty inventory. Invalid or unowned names are +// ignored. +runtime::Result*> inventory_cache_keys(NSString* root); + +enum class CacheBookmarkState { + Missing, + Corrupt, + Present, +}; + +struct CacheBookmarkInspection final { + CacheBookmarkState state; + NSData* data; +}; enum class CacheEntryPersistence { BookmarkCapable, @@ -45,6 +59,14 @@ class CacheEntryLock final { // Missing staging is a no-op; unexpected entry types fail before deletion. runtime::Error remove_staging() const; + // Maintenance accepts malformed backend-owned entries but never follows + // them. The Strict runtime cleanup above is intentionally separate. + runtime::Result inspect_bookmark_for_maintenance() + const; + runtime::Error preflight_maintenance() const; + runtime::Error remove_staging_for_maintenance() const; + runtime::Error remove_bookmark_for_maintenance() const; + private: friend runtime::Result> acquire_cache_entry_lock( diff --git a/backends/apple/coreai/runtime/coreai_bookmarks.mm b/backends/apple/coreai/runtime/coreai_bookmarks.mm index 393653028af..cad63fa6e35 100644 --- a/backends/apple/coreai/runtime/coreai_bookmarks.mm +++ b/backends/apple/coreai/runtime/coreai_bookmarks.mm @@ -38,7 +38,8 @@ Result open_root(NSString* root) { const int fd = retry_eintr([&] { return open( - root.fileSystemRepresentation, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + root.fileSystemRepresentation, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); }); if (fd < 0 && errno == ENOENT) { return -1; @@ -190,6 +191,53 @@ FileDescriptor lock_file(open_storage_shared_file( return path; } +Result*> inventory_cache_keys(NSString* root) { + auto resolved = + resolve_bookmark_root(root, StorageCreationMode::OpenExisting); + if (!resolved.ok()) { + return resolved.error(); + } + auto opened = open_root(resolved.get()); + if (!opened.ok()) { + return opened.error(); + } + FileDescriptor directory(opened.get()); + if (directory.get() < 0) { + return @[]; + } + NSMutableSet* keys = [NSMutableSet set]; + for (NSString* child_name in @[ kBookmarksDirectory, kStagingDirectory ]) { + auto child = open_child( + directory.get(), child_name, StorageCreationMode::OpenExisting); + if (!child.ok()) { + return child.error(); + } + FileDescriptor child_directory(child.get()); + if (child_directory.get() < 0) { + continue; + } + auto names = + storage_children(child_directory.get(), StorageNamePolicy::SkipInvalid); + if (!names.ok()) { + return names.error(); + } + for (NSString* name in names.get()) { + NSString* key = name; + if ([child_name isEqual:kBookmarksDirectory]) { + if (![name hasSuffix:kBookmarkExtension] || + name.length <= kBookmarkExtension.length) { + continue; + } + key = [name substringToIndex:name.length - kBookmarkExtension.length]; + } + if (is_cache_key(key)) { + [keys addObject:key]; + } + } + } + return [[keys allObjects] sortedArrayUsingSelector:@selector(compare:)]; +} + CacheEntryLock::CacheEntryLock( int root, int bookmarks, @@ -282,6 +330,77 @@ FileDescriptor file(retry_eintr([&] { staging.get(), key_, StorageRemovalPolicy::Strict); } +Result +CacheEntryLock::inspect_bookmark_for_maintenance() const { + if (persistence_ == CacheEntryPersistence::StagingOnly) { + return CacheBookmarkInspection{CacheBookmarkState::Missing, nil}; + } + struct stat info; + const int inspected = lstat_at( + bookmarks_.get(), bookmark_name(key_).fileSystemRepresentation, &info); + if (inspected < 0 && errno == ENOENT) { + return CacheBookmarkInspection{CacheBookmarkState::Missing, nil}; + } + ET_CHECK_OR_RETURN_ERROR( + inspected == 0, AccessFailed, "Cannot inspect Core AI bookmark"); + if (!S_ISREG(info.st_mode)) { + return CacheBookmarkInspection{CacheBookmarkState::Corrupt, nil}; + } + auto bookmark = read_bookmark(); + if (!bookmark.ok()) { + if (bookmark.error() == Error::InvalidExternalData) { + return CacheBookmarkInspection{CacheBookmarkState::Corrupt, nil}; + } + return bookmark.error(); + } + return CacheBookmarkInspection{ + bookmark.get() == nil ? CacheBookmarkState::Missing + : CacheBookmarkState::Present, + bookmark.get()}; +} + +Error CacheEntryLock::preflight_maintenance() const { + if (persistence_ == CacheEntryPersistence::BookmarkCapable) { + ET_CHECK_OK_OR_RETURN_ERROR(preflight_storage_directory( + bookmarks_.get(), + bookmark_name(key_), + StorageRemovalPolicy::Maintenance)); + } + auto opened = open_child( + root_.get(), kStagingDirectory, StorageCreationMode::OpenExisting); + if (!opened.ok()) { + return opened.error(); + } + FileDescriptor staging(opened.get()); + if (staging.get() < 0) { + return Error::Ok; + } + return preflight_storage_directory( + staging.get(), key_, StorageRemovalPolicy::Maintenance); +} + +Error CacheEntryLock::remove_staging_for_maintenance() const { + auto opened = open_child( + root_.get(), kStagingDirectory, StorageCreationMode::OpenExisting); + if (!opened.ok()) { + return opened.error(); + } + FileDescriptor staging(opened.get()); + if (staging.get() < 0) { + return Error::Ok; + } + return remove_storage_directory( + staging.get(), key_, StorageRemovalPolicy::Maintenance); +} + +Error CacheEntryLock::remove_bookmark_for_maintenance() const { + if (persistence_ == CacheEntryPersistence::StagingOnly) { + return Error::Ok; + } + return remove_storage_directory( + bookmarks_.get(), bookmark_name(key_), StorageRemovalPolicy::Maintenance); +} + Result> lock_cache_entry( NSString* root, NSString* key) { diff --git a/backends/apple/coreai/runtime/coreai_cache.h b/backends/apple/coreai/runtime/coreai_cache.h index b6ce1c06258..94e14968bc7 100644 --- a/backends/apple/coreai/runtime/coreai_cache.h +++ b/backends/apple/coreai/runtime/coreai_cache.h @@ -33,12 +33,10 @@ namespace executorch::backends::coreai { /** * Clears all recorded cache variants referenced by every CoreAI delegate in - * the PTE after validating its manifests. Discovery does not require host - * compatibility or SDK availability. Actual SDK eviction requires a loader. - * Missing/corrupt attribution can leave legacy source entries unidentified: - * known matches are cleared, then an error reports incomplete cleanup. A - * root-wide clear can recover these entries. Does not initialize delegates or - * materialize Core AI assets. + * the PTE after validating its manifests. Model prefixes select every device + * and specialization variant. Discovery does not require host compatibility or + * SDK availability. Actual SDK eviction requires a loader. Does not initialize + * delegates or materialize Core AI assets. * * A null directory selects the same Caches default as backend initialization. * Supplied directories must be valid explicit roots; they never fall back to diff --git a/backends/apple/coreai/runtime/coreai_cache.mm b/backends/apple/coreai/runtime/coreai_cache.mm index f20155dd843..369a5164a88 100644 --- a/backends/apple/coreai/runtime/coreai_cache.mm +++ b/backends/apple/coreai/runtime/coreai_cache.mm @@ -7,10 +7,12 @@ */ #include "coreai_cache.h" -#import "coreai_assets.h" +#import "coreai_bookmarks.h" +#import "coreai_cache_key.h" +#import "coreai_cache_root.h" #import "coreai_load_coordinator.h" +#import "coreai_manifest.h" #include "coreai_pte.h" -#import "coreai_storage.h" #include @@ -23,13 +25,14 @@ NSString* root = nil; if (path == nullptr) { auto default_root = default_coreai_assets_root(); - if (!default_root.ok()) + if (!default_root.ok()) { return default_root.error(); + } root = default_root.get(); } else { root = [NSString stringWithUTF8String:path]; } - return resolve_bookmark_root(root, false); + return resolve_bookmark_root(root, StorageCreationMode::OpenExisting); } Error clear_keys(NSString* root, NSArray* keys) { @@ -37,113 +40,76 @@ Error clear_keys(NSString* root, NSArray* keys) { sortedArrayUsingSelector:@selector(compare:)]; Error first_error = Error::Ok; for (NSString* key in ordered) { - Error error = clear_bookmark_assets(root, key, nil); + const Error error = clear_bookmark_assets(root, key, nil); if (error != Error::Ok) { ET_LOG( Error, "Core AI cache clear failed for key %s: 0x%x", key.UTF8String, static_cast(error)); - if (first_error == Error::Ok) + if (first_error == Error::Ok) { first_error = error; + } } } return first_error; } Error clear_pte(runtime::DataLoader& loader, NSString* root) { - // Keep the verified image and original segment owners alive through eviction. + // Keep the verified image and selected segment owners alive through eviction. auto pte = inspect_coreai_pte(loader); - if (!pte.ok()) + if (!pte.ok()) { return pte.error(); - NSMutableSet* identities = [NSMutableSet set]; - NSMutableSet* legacy_aot_keys = [NSMutableSet set]; - bool has_source = false; + } + NSMutableSet* model_prefixes = [NSMutableSet set]; for (const auto& buffer : pte->processed) { auto data = buffer.data_safe(); - if (!data.ok()) + if (!data.ok()) { return data.error(); + } ET_CHECK_OR_RETURN_ERROR( data.get() != nullptr && buffer.size() > 0, InvalidProgram, "Missing Core AI manifest"); - NSData* bytes = [NSData dataWithBytesNoCopy:const_cast(data.get()) - length:buffer.size() - freeWhenDone:NO]; - auto manifest = parse_manifest(bytes); - if (!manifest.ok()) + auto manifest = parse_manifest(data.get(), buffer.size()); + if (!manifest.ok()) { return manifest.error(); - for (NSString* bundle in manifest->bundle_digests) { - auto identity = bookmark_identity(manifest.get(), bundle); - if (!identity.ok()) - return identity.error(); - [identities addObject:identity.get()]; } - if (manifest->aot_compiled) { - for (NSString* architecture in manifest->archs) { - auto selected = - select_assets(manifest.get(), architecture, manifest->platform); - if (!selected.ok()) - return selected.error(); - auto key = - bookmark_key(selected.get(), manifest->platform, architecture); - if (!key.ok()) - return key.error(); - [legacy_aot_keys addObject:key.get()]; + for (NSString* bundle_name in manifest->bundle_digests) { + auto prefix = cache_model_key(manifest.get(), bundle_name); + if (!prefix.ok()) { + return prefix.error(); } - } else { - has_source = true; + [model_prefixes addObject:prefix.get()]; } } - if (identities.count == 0) + if (model_prefixes.count == 0) { return Error::Ok; - auto inventory = inventory_bookmark_keys(root); - if (!inventory.ok()) + } + auto inventory = inventory_cache_keys(root); + if (!inventory.ok()) { return inventory.error(); + } NSMutableArray* keys = [NSMutableArray array]; - Error lookup_error = Error::Ok; for (NSString* key in inventory.get()) { - if ([legacy_aot_keys containsObject:key]) { + if ([model_prefixes containsObject:cache_key_model_prefix(key)]) { [keys addObject:key]; - continue; - } - auto locked = lock_bookmark(root, key, false); - if (!locked.ok()) { - if (lookup_error == Error::Ok) - lookup_error = locked.error(); - continue; } - if (locked.get() == nullptr) - continue; - auto identity = read_bookmark_identity(*locked.get()); - if (!identity.ok() || identity.get() == nil) { - if (has_source) { - ET_LOG( - Info, - "Core AI cache key %s has missing or invalid attribution; PTE cleanup is incomplete", - key.UTF8String); - if (lookup_error == Error::Ok) - lookup_error = - identity.ok() ? Error::InvalidExternalData : identity.error(); - } - continue; - } - if ([identities containsObject:identity.get()]) - [keys addObject:key]; } - const auto cleared = clear_keys(root, keys); - return lookup_error != Error::Ok ? lookup_error : cleared; + return clear_keys(root, keys); } } // namespace Error clear_cache(const char* coreai_assets_dir) { @autoreleasepool { auto root = cache_root(coreai_assets_dir); - if (!root.ok()) + if (!root.ok()) { return root.error(); - auto keys = inventory_bookmark_keys(root.get()); - if (!keys.ok()) + } + auto keys = inventory_cache_keys(root.get()); + if (!keys.ok()) { return keys.error(); + } return clear_keys(root.get(), keys.get()); } } @@ -153,8 +119,9 @@ Error clear_cache_for_pte( const char* coreai_assets_dir) { @autoreleasepool { auto root = cache_root(coreai_assets_dir); - if (!root.ok()) + if (!root.ok()) { return root.error(); + } return clear_pte(loader, root.get()); } } @@ -165,8 +132,9 @@ Error clear_cache_for_pte(const char* pte_path, const char* coreai_assets_dir) { InvalidArgument, "Missing PTE path"); auto loader = extension::FileDataLoader::from(pte_path); - if (!loader.ok()) + if (!loader.ok()) { return loader.error(); + } return clear_cache_for_pte(loader.get(), coreai_assets_dir); } diff --git a/backends/apple/coreai/runtime/coreai_fault_injection.h b/backends/apple/coreai/runtime/coreai_fault_injection.h index 4981d05cdd6..4e72901c775 100644 --- a/backends/apple/coreai/runtime/coreai_fault_injection.h +++ b/backends/apple/coreai/runtime/coreai_fault_injection.h @@ -23,6 +23,8 @@ enum class FaultPoint { BeforeCacheEntryLock, BeforeSDK, AfterSDK, + BeforeEvict, + AfterEvict, }; #if defined(COREAI_ASSETS_TESTING) && COREAI_ASSETS_TESTING diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.mm b/backends/apple/coreai/runtime/coreai_load_coordinator.mm index d7efbcd8686..ab0d7149fb4 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.mm +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.mm @@ -82,7 +82,7 @@ void cleanup_source(const CacheEntryLock& cache_entry) { Error evict_one(id loader, NSData* bookmark) { ET_CHECK_OR_RETURN_ERROR( - storage_fault(StorageOperation::BeforeEvict) == 0, + fault_point(FaultPoint::BeforeEvict) == 0, AccessFailed, "Core AI eviction interrupted"); dispatch_semaphore_t ready = dispatch_semaphore_create(0); @@ -93,17 +93,21 @@ Error evict_one(id loader, NSData* bookmark) { dispatch_semaphore_signal(ready); }]; dispatch_semaphore_wait(ready, DISPATCH_TIME_FOREVER); - if (failure != nil) { - // Probe after failed deletion so our own probe cannot make the entry busy. + NSError* eviction_failure = failure; + // A loader may retain the completion block after invoking it. + failure = nil; + if (eviction_failure != nil) { + // A confirmed restore miss proves that a failed eviction is already done. @autoreleasepool { auto probe = restore(loader, bookmark); - if (probe.ok() && probe.get() == nil) + if (probe.ok() && probe.get() == nil) { return Error::Ok; + } } - return sdk_error(failure); + return acquisition_error(eviction_failure); } ET_CHECK_OR_RETURN_ERROR( - storage_fault(StorageOperation::AfterEvict) == 0, + fault_point(FaultPoint::AfterEvict) == 0, AccessFailed, "Core AI eviction interrupted"); return Error::Ok; @@ -216,28 +220,27 @@ Error clear_bookmark_assets( NSString* root, NSString* key, id loader) { - auto locked = lock_bookmark(root, key, false); - if (!locked.ok()) + auto locked = lock_existing_cache_entry(root, key); + if (!locked.ok()) { return locked.error(); - if (locked.get() == nullptr) + } + if (locked.get() == nullptr) { return Error::Ok; + } ET_LOG(Info, "Core AI cache clear started: key=%s", key.UTF8String); - auto bookmark = read_bookmark(*locked.get(), /*maintenance=*/true); - if (!bookmark.ok() && bookmark.error() != Error::InvalidExternalData) { + auto bookmark = locked.get()->inspect_bookmark_for_maintenance(); + if (!bookmark.ok()) { return bookmark.error(); } - NSData* bytes = bookmark.ok() ? bookmark.get() : nil; - if (bytes == nil) { + ET_CHECK_OK_OR_RETURN_ERROR(locked.get()->preflight_maintenance()); + if (bookmark->state != CacheBookmarkState::Present) { ET_LOG( Info, "Core AI cache key %s has a %s bookmark; SDK eviction could not be " "confirmed; attempting local cleanup", key.UTF8String, - bookmark.ok() ? "missing" : "corrupt"); - } - ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_staging( - *locked.get(), false, StorageRemovalMode::Maintenance)); - if (bytes != nil) { + bookmark->state == CacheBookmarkState::Missing ? "missing" : "corrupt"); + } else { if (loader == nil) { ET_CHECK_OR_RETURN_ERROR( ETCoreAIIsAvailable(), @@ -248,18 +251,14 @@ Error clear_bookmark_assets( loader != nil, Internal, "Cannot create Core AI model loader"); } ET_LOG(Info, "Core AI SDK eviction requested: key=%s", key.UTF8String); - ET_CHECK_OK_OR_RETURN_ERROR(evict_one(loader, bytes)); + ET_CHECK_OK_OR_RETURN_ERROR(evict_one(loader, bookmark->data)); ET_LOG( Info, "Core AI SDK cache entry absence confirmed: key=%s", key.UTF8String); } - ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_staging( - *locked.get(), true, StorageRemovalMode::Maintenance)); - if (!bookmark.ok() || bytes != nil) { - ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark(*locked.get())); - } - ET_CHECK_OK_OR_RETURN_ERROR(remove_bookmark_identity(*locked.get())); + ET_CHECK_OK_OR_RETURN_ERROR(locked.get()->remove_staging_for_maintenance()); + ET_CHECK_OK_OR_RETURN_ERROR(locked.get()->remove_bookmark_for_maintenance()); ET_LOG(Info, "Core AI local cache entry cleared: key=%s", key.UTF8String); return Error::Ok; } diff --git a/backends/apple/coreai/runtime/coreai_storage.h b/backends/apple/coreai/runtime/coreai_storage.h index 33bd34d3272..a7fed0578ed 100644 --- a/backends/apple/coreai/runtime/coreai_storage.h +++ b/backends/apple/coreai/runtime/coreai_storage.h @@ -52,6 +52,14 @@ enum class StorageRemovalPolicy { Maintenance, }; +// Checks one child tree without following links or changing storage. Missing +// entries are already removed. Maintenance accepts malformed backend-owned +// non-directory entries. +runtime::Error preflight_storage_directory( + int parent_fd, + NSString* name, + StorageRemovalPolicy policy = StorageRemovalPolicy::Strict); + // Removes one child directory without following links. The complete tree is // checked before removal and every entry is checked again during removal. // Missing entries are already removed. Maintenance also removes malformed diff --git a/backends/apple/coreai/runtime/coreai_storage.mm b/backends/apple/coreai/runtime/coreai_storage.mm index f30c992081d..553eff015b1 100644 --- a/backends/apple/coreai/runtime/coreai_storage.mm +++ b/backends/apple/coreai/runtime/coreai_storage.mm @@ -259,6 +259,37 @@ FileDescriptor copy(retry_eintr( return [names sortedArrayUsingSelector:@selector(compare:)]; } +Error preflight_storage_directory( + int parent_fd, + NSString* name, + StorageRemovalPolicy policy) { + ET_CHECK_OR_RETURN_ERROR( + storage_component(name), InvalidArgument, "Invalid Core AI entry name"); + struct stat info; + const int inspected = + lstat_at(parent_fd, name.fileSystemRepresentation, &info); + if (inspected < 0 && errno == ENOENT) { + return Error::Ok; + } + ET_CHECK_OR_RETURN_ERROR( + inspected == 0, AccessFailed, "Cannot inspect Core AI storage entry"); + if (!S_ISDIR(info.st_mode)) { + ET_CHECK_OR_RETURN_ERROR( + policy == StorageRemovalPolicy::Maintenance, + InvalidExternalData, + "Core AI storage entry is not a directory"); + return Error::Ok; + } + FileDescriptor entry( + open_directory_at(parent_fd, name.fileSystemRepresentation)); + if (entry.get() < 0 && errno == ENOENT) { + return Error::Ok; + } + ET_CHECK_OR_RETURN_ERROR( + entry.get() >= 0, AccessFailed, "Cannot open Core AI storage directory"); + return walk_storage_tree(entry.get(), StorageTreePass::Preflight, policy); +} + Error remove_storage_directory( int parent_fd, NSString* name, diff --git a/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm b/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm index 5734fa49a84..d331194a2f3 100644 --- a/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm @@ -803,103 +803,4 @@ void expect_order( EXPECT_EQ(logs.find("Core AI cache bookmark saved"), std::string::npos); } -TEST_F(CoreAILifecycleLogTest, FinalBookmarkFlushFailureDoesNotLogDurableSave) { - BookmarkLoaderScope fake; - { - auto locked = lock_bookmark(root.url.path, key); - ASSERT_TRUE(locked.ok()); - ASSERT_EQ( - ensure_bookmark_identity(*locked.get(), manifest, @"macOS", @"arch_b"), - Error::Ok); - } - StorageFaultScope fault(StorageOperation::FullSync); - fault.match_index = 5; - const auto logs = - capture([&] { return acquire(fake.loader, &data); }, Error::AccessFailed); - EXPECT_EQ(fault.hits, 1); - EXPECT_NE(logs.find("Core AI specialization succeeded:"), std::string::npos); - EXPECT_EQ(logs.find("Core AI cache bookmark saved"), std::string::npos); - EXPECT_TRUE([saved() isEqual:fake.loader.lastAcquired.bookmark]); -} - -TEST_F(CoreAILifecycleLogTest, ClearLogsConfirmedAbsenceBeforeLocalCompletion) { - BookmarkLoaderScope fake; - ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); - const auto logs = capture( - [&] { return clear_bookmark_assets(root.url.path, key, fake.loader); }); - expect_order( - logs, - { - "Core AI cache clear started:", - "Core AI SDK eviction requested:", - "Core AI SDK cache entry absence confirmed:", - "Core AI local cache entry cleared:", - }); - EXPECT_EQ(saved(), nil); -} - -TEST_F(CoreAILifecycleLogTest, FailedEvictionWithConfirmedMissLogsAbsence) { - BookmarkLoaderScope fake; - ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); - fake.loader.evictError = [NSError errorWithDomain:NSPOSIXErrorDomain - code:ENOENT - userInfo:nil]; - fake.loader.restoreMiss = YES; - const auto logs = capture( - [&] { return clear_bookmark_assets(root.url.path, key, fake.loader); }); - expect_order( - logs, - { - "Core AI SDK eviction requested:", - "Core AI SDK cache entry absence confirmed:", - "Core AI local cache entry cleared:", - }); -} - -TEST_F(CoreAILifecycleLogTest, EvictionErrorDoesNotLogAbsenceOrCompletion) { - BookmarkLoaderScope fake; - ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); - fake.loader.evictError = [NSError errorWithDomain:NSPOSIXErrorDomain - code:EBUSY - userInfo:nil]; - const auto logs = capture( - [&] { return clear_bookmark_assets(root.url.path, key, fake.loader); }, - Error::Internal); - EXPECT_NE(logs.find("Core AI SDK eviction requested:"), std::string::npos); - EXPECT_EQ( - logs.find("Core AI SDK cache entry absence confirmed:"), - std::string::npos); - EXPECT_EQ(logs.find("Core AI local cache entry cleared:"), std::string::npos); - EXPECT_NE(saved(), nil); -} - -TEST_F( - CoreAILifecycleLogTest, - IdentityRemovalFailureDoesNotLogLocalCompletion) { - BookmarkLoaderScope fake; - ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); - StorageFaultScope fault(StorageOperation::Remove); - fault.match_index = 2; - const auto logs = capture( - [&] { return clear_bookmark_assets(root.url.path, key, fake.loader); }, - Error::AccessFailed); - EXPECT_EQ(fault.hits, 1); - EXPECT_NE( - logs.find("Core AI SDK cache entry absence confirmed:"), - std::string::npos); - EXPECT_EQ(logs.find("Core AI local cache entry cleared:"), std::string::npos); - auto locked = lock_bookmark(root.url.path, key, false); - ASSERT_TRUE(locked.ok()); - ASSERT_NE(locked.get(), nullptr); - auto identity = read_bookmark_identity(*locked.get()); - ASSERT_TRUE(identity.ok()); - EXPECT_NE(identity.get(), nil); -} - -TEST_F(CoreAILifecycleLogTest, AbsentEntryClearRemainsQuiet) { - const auto logs = - capture([&] { return clear_bookmark_assets(root.url.path, key, nil); }); - EXPECT_TRUE(logs.empty()) << logs; -} - } // namespace diff --git a/backends/apple/coreai/runtime/test/coreai_cache_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_test.mm index dc323122c0a..0d8f732e2ee 100644 --- a/backends/apple/coreai/runtime/test/coreai_cache_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_test.mm @@ -68,7 +68,7 @@ AssertionResult seed_cache( bool stage = true, NSString* architecture = @"arch_b", NSString* platform = @"macOS", - bool metadata = true) { + NSString* specialization = @"default") { if (root == nil || loader == nil) { return AssertionFailure() << "Cache seed requires a root and fake loader"; } @@ -80,7 +80,8 @@ AssertionResult seed_cache( if (!selected.ok()) { return AssertionFailure() << "Cannot select cache seed assets"; } - auto key = bookmark_key(selected.get(), platform, architecture); + auto key = + cache_variant_key(selected.get(), platform, architecture, specialization); if (!key.ok()) { return AssertionFailure() << "Cannot compute cache seed key"; } @@ -89,26 +90,17 @@ AssertionResult seed_cache( nil, [[root URLByAppendingPathComponent:@"staging"] URLByAppendingPathComponent:key.get()]}; - auto lock = lock_bookmark(root.path, entry.key); + auto lock = lock_cache_entry(root.path, entry.key); if (!lock.ok()) { return AssertionFailure() << "Cannot lock cache seed bookmark"; } - if (metadata && - ensure_bookmark_identity( - *lock.get(), selected.get(), platform, architecture) != Error::Ok) { - return AssertionFailure() << "Cannot attribute cache seed"; - } if (stage) { - auto staging = prepare_bookmark_staging(*lock.get()); - if (!staging.ok()) { - return AssertionFailure() << "Cannot prepare cache seed staging"; - } TestData data; if (selected->aot_compiled) { aot_data(data, architecture); } auto staged = executorch::backends::coreai::prepare_source_bundle( - selected.get(), &data, staging.get(), entry.key); + selected.get(), &data, *lock.get(), CorruptBundlePolicy::Reject); if (data.requests != data.releases) { return AssertionFailure() << "Cache seed source buffers were not released"; @@ -127,7 +119,7 @@ AssertionResult seed_cache( model.owner = loader; model.bookmark = entry.bookmark; loader.models[entry.bookmark] = model; - if (write_bookmark(*lock.get(), entry.bookmark) != Error::Ok) { + if (lock.get()->write_bookmark(entry.bookmark) != Error::Ok) { return AssertionFailure() << "Cannot write cache seed bookmark"; } } @@ -139,16 +131,10 @@ bool cache_exists(NSURL* url) { return [NSFileManager.defaultManager fileExistsAtPath:url.path]; } -NSURL* cache_identity_url(NSURL* root, NSString* key) { - return [[test_bookmark_url(root, key) URLByDeletingPathExtension] - URLByAppendingPathExtension:@"metadata"]; -} - void cache_entry_removed(NSURL* root, const CacheEntry& entry) { - EXPECT_FALSE(cache_exists(cache_identity_url(root, entry.key))); EXPECT_FALSE(cache_exists(test_bookmark_url(root, entry.key))); EXPECT_FALSE(cache_exists(entry.stage)); - EXPECT_TRUE(cache_exists(test_bookmark_lock_url(root, entry.key))); + EXPECT_TRUE(cache_exists(test_cache_lock_url(root, entry.key))); } NSMutableDictionary* cache_manifest(char identity) { @@ -384,7 +370,7 @@ FBSyntheticPTE pte( } } -TEST_F(CoreAICacheTest, ClearsLegacyAotWithoutAttribution) { +TEST_F(CoreAICacheTest, ClearsAotBundles) { @autoreleasepool { TestDirectory root; BookmarkLoaderScope fake; @@ -399,7 +385,7 @@ FBSyntheticPTE pte( false, @"arch_a", @"macOS", - false)); + @"default")); ASSERT_TRUE(seed_cache( root.url, dict, @@ -409,7 +395,7 @@ FBSyntheticPTE pte( false, @"arch_b", @"macOS", - false)); + @"default")); FBSyntheticPTE pte({{"forward", {{"CoreAIBackend", encode(dict)}}}}); EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); EXPECT_EQ(fake.loader->evictions.load(), 2); @@ -418,60 +404,56 @@ FBSyntheticPTE pte( } } -TEST_F( - CoreAICacheTest, - UnattributedEntriesReportIncompleteButClearKnownMatches) { - for (bool corrupt : {false, true}) { - SCOPED_TRACE(corrupt); - @autoreleasepool { - TestDirectory root; - BookmarkLoaderScope fake; - CacheEntry unknown, known; - ASSERT_TRUE(seed_cache( - root.url, - manifest_dict(), - fake.loader, - unknown, - true, - false, - @"old_arch", - @"macOS", - corrupt)); - ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, known)); - if (corrupt) { - ASSERT_TRUE([[@"{}" dataUsingEncoding:NSUTF8StringEncoding] - writeToURL:cache_identity_url(root.url, unknown.key) - options:0 - error:nil]); - } - FBSyntheticPTE pte( - {{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); - EXPECT_EQ( - clear_cache_for_pte(pte, root.url.path.UTF8String), - Error::InvalidExternalData); - EXPECT_EQ(fake.loader->evictions.load(), 1); - cache_entry_removed(root.url, known); - EXPECT_TRUE(cache_exists(test_bookmark_url(root.url, unknown.key))); - EXPECT_NE(fake.loader.models[unknown.bookmark], nil); - EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::Ok); - cache_entry_removed(root.url, unknown); - } - } -} - -TEST_F(CoreAICacheTest, ClearsAttributionOnlyAndStagingEntriesWithoutSdk) { +TEST_F(CoreAICacheTest, ClearsOptionAndDeviceVariantsPreservingUnrelatedModel) { @autoreleasepool { TestDirectory root; BookmarkLoaderScope fake; - CacheEntry metadata_only, staged; + CacheEntry base, specialized, device, unrelated; + ASSERT_TRUE(seed_cache(root.url, manifest_dict(), fake.loader, base)); ASSERT_TRUE(seed_cache( root.url, manifest_dict(), fake.loader, - metadata_only, - false, - false, + specialized, + true, + true, + @"arch_b", + @"macOS", + @"tuned")); + ASSERT_TRUE(seed_cache( + root.url, + manifest_dict(), + fake.loader, + device, + true, + true, @"other_arch")); + ASSERT_TRUE( + seed_cache(root.url, cache_manifest('3'), fake.loader, unrelated)); + FBSyntheticPTE pte( + {{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); + EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); + drain_callbacks(); + EXPECT_EQ(fake.loader->evictions.load(), 3); + EXPECT_EQ(fake.loader->specializationQueries.load(), 0); + cache_entry_removed(root.url, base); + cache_entry_removed(root.url, specialized); + cache_entry_removed(root.url, device); + EXPECT_TRUE(cache_exists(test_bookmark_url(root.url, unrelated.key))); + EXPECT_TRUE(cache_exists(unrelated.stage)); + EXPECT_NE(fake.loader.models[unrelated.bookmark], nil); + EXPECT_EQ(fake.loader.models[base.bookmark], nil); + EXPECT_EQ(fake.loader.models[specialized.bookmark], nil); + EXPECT_EQ(fake.loader.models[device.bookmark], nil); + EXPECT_TRUE(pte.check_released()); + } +} + +TEST_F(CoreAICacheTest, ClearsStagingOnlyEntriesWithoutSdk) { + @autoreleasepool { + TestDirectory root; + BookmarkLoaderScope fake; + CacheEntry staged; ASSERT_TRUE(seed_cache( root.url, manifest_dict(), fake.loader, staged, false, true)); bridge.state().available = false; @@ -479,7 +461,6 @@ FBSyntheticPTE pte( {{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); EXPECT_EQ(fake.loader->evictions.load(), 0); - cache_entry_removed(root.url, metadata_only); cache_entry_removed(root.url, staged); } } @@ -726,7 +707,7 @@ FBSyntheticPTE pte( ASSERT_TRUE([NSData.data writeToURL:path options:0 error:nil]); bridge.state().available = false; { - StorageFaultScope fault(StorageOperation::Remove); + StorageFaultScope fault(FaultPoint::Remove); EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::AccessFailed); EXPECT_EQ(fault.hits, 1); EXPECT_TRUE(cache_exists(path)); @@ -798,7 +779,7 @@ FBSyntheticPTE pte( ASSERT_TRUE( seed_cache(root.url, aot_manifest_dict(), fake.loader, entry)); { - StorageFaultScope fault(StorageOperation::Remove); + StorageFaultScope fault(FaultPoint::Remove); fault.match_index = row.match_index; EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::AccessFailed); EXPECT_EQ(fault.hits, 1); @@ -819,33 +800,6 @@ FBSyntheticPTE pte( } } -TEST_F(CoreAICacheTest, AttributionRemovalFailureLeavesDiscoverableRetry) { - @autoreleasepool { - TestDirectory root; - BookmarkLoaderScope fake; - CacheEntry entry; - ASSERT_TRUE( - seed_cache(root.url, manifest_dict(), fake.loader, entry, true, false)); - FBSyntheticPTE pte( - {{"forward", {{"CoreAIBackend", encode(manifest_dict())}}}}); - { - StorageFaultScope fault(StorageOperation::Remove); - fault.match_index = 2; - EXPECT_EQ( - clear_cache_for_pte(pte, root.url.path.UTF8String), - Error::AccessFailed); - EXPECT_EQ(fault.hits, 1); - } - EXPECT_FALSE(cache_exists(test_bookmark_url(root.url, entry.key))); - EXPECT_TRUE(cache_exists(cache_identity_url(root.url, entry.key))); - EXPECT_EQ(fake.loader->evictions.load(), 1); - bridge.state().available = false; - EXPECT_EQ(clear_cache_for_pte(pte, root.url.path.UTF8String), Error::Ok); - EXPECT_EQ(fake.loader->evictions.load(), 1); - cache_entry_removed(root.url, entry); - } -} - TEST_F(CoreAICacheTest, IndependentKeysContinueInSortedOrderAfterFailures) { @autoreleasepool { TestDirectory root; @@ -869,7 +823,7 @@ FBSyntheticPTE pte( ? [NSError errorWithDomain:NSPOSIXErrorDomain code:EBUSY userInfo:nil] : nil; }; - StorageFaultScope fault(StorageOperation::BeforeEvict); + StorageFaultScope fault(FaultPoint::BeforeEvict); EXPECT_EQ(clear_cache(root.url.path.UTF8String), Error::AccessFailed); drain_callbacks(); EXPECT_EQ(fault.hits, 1); diff --git a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm index c14447b31c2..2585d23f64d 100644 --- a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm @@ -184,9 +184,9 @@ - (void)clearRetainedState { BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); TestData data; - auto selected = bookmark_manifest(); + auto selected = bookmark_bundle(); ASSERT_TRUE(selected.ok()); - auto key_result = test_bookmark_key(selected.get()); + auto key_result = test_cache_key(selected.get()); ASSERT_TRUE(key_result.ok()); NSString* key = key_result.get(); NSMutableDictionary* dict = manifest_dict(); @@ -249,9 +249,9 @@ - (void)clearRetainedState { BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); TestData data; - auto selected = bookmark_manifest(); + auto selected = bookmark_bundle(); ASSERT_TRUE(selected.ok()); - auto key_result = test_bookmark_key(selected.get()); + auto key_result = test_cache_key(selected.get()); ASSERT_TRUE(key_result.ok()); NSString* key = key_result.get(); EXPECT_EQ( @@ -259,7 +259,7 @@ - (void)clearRetainedState { EXPECT_EQ(fake.loader->evictions.load(), 0); // Retain staging after publication to exercise clearing's failure paths. { - StorageFaultScope fault(StorageOperation::Remove); + StorageFaultScope fault(FaultPoint::Remove); ASSERT_EQ( initialize_and_destroy(manifest_dict(), &data, root.url.path), Error::Ok); @@ -290,7 +290,7 @@ - (void)clearRetainedState { fake.loader.restoreError = nil; fake.loader.evictError = nil; { - StorageFaultScope fault(StorageOperation::AfterEvict); + StorageFaultScope fault(FaultPoint::AfterEvict); EXPECT_NE( clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); EXPECT_EQ(fault.hits, 1); @@ -311,7 +311,7 @@ - (void)clearRetainedState { initialize_and_destroy(manifest_dict(), &data, root.url.path), Error::Ok); { - StorageFaultScope fault(StorageOperation::Remove); + StorageFaultScope fault(FaultPoint::Remove); EXPECT_NE( clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); EXPECT_EQ(fault.hits, 1); @@ -337,9 +337,9 @@ - (void)clearRetainedState { TestDirectory root; ASSERT_NE(root.url, nil); TestData data; - auto selected = bookmark_manifest(); + auto selected = bookmark_bundle(); ASSERT_TRUE(selected.ok()); - auto key_result = test_bookmark_key(selected.get()); + auto key_result = test_cache_key(selected.get()); ASSERT_TRUE(key_result.ok()); NSString* key = key_result.get(); PinBookmarkLoader* loader = [[PinBookmarkLoader alloc] init]; @@ -347,9 +347,10 @@ - (void)clearRetainedState { // Scope teardown clears retained completions even after fatal test // failures. bridge.state().bookmark_loader = loader; + RuntimeAssets assets{selected.get(), @"macOS", @"arch_b"}; @autoreleasepool { - auto pinned = acquire_bookmark_model( - selected.get(), &data, root.url.path, @"macOS", @"arch_b", loader); + auto pinned = + acquire_bookmark_model(assets, &data, root.url.path, loader); ASSERT_TRUE(pinned.ok()); EXPECT_NE(loader.temporary, nil); EXPECT_NE(clear_bookmark_assets(root.url.path, key, loader), Error::Ok); @@ -381,9 +382,9 @@ - (void)clearRetainedState { BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); TestData data; - auto selected = bookmark_manifest(); + auto selected = bookmark_bundle(); ASSERT_TRUE(selected.ok()); - auto key_result = test_bookmark_key(selected.get()); + auto key_result = test_cache_key(selected.get()); ASSERT_TRUE(key_result.ok()); NSString* key = key_result.get(); ASSERT_EQ( @@ -407,15 +408,11 @@ - (void)clearRetainedState { } dispatch_semaphore_signal(eviction_done); }; + RuntimeAssets assets{selected.get(), @"macOS", @"arch_b"}; auto load = [&] { @autoreleasepool { - auto acquired = acquire_bookmark_model( - selected.get(), - &data, - root.url.path, - @"macOS", - @"arch_b", - fake.loader); + auto acquired = + acquire_bookmark_model(assets, &data, root.url.path, fake.loader); load_ok = acquired.ok() && acquired.get() != nil; } dispatch_semaphore_signal(loading_observed); diff --git a/backends/apple/coreai/runtime/test/coreai_fake_loader.h b/backends/apple/coreai/runtime/test/coreai_fake_loader.h index 6f03c9f78b1..5fb287ed79a 100644 --- a/backends/apple/coreai/runtime/test/coreai_fake_loader.h +++ b/backends/apple/coreai/runtime/test/coreai_fake_loader.h @@ -91,6 +91,7 @@ class ScopedFakeBridgeState final { std::atomic specializations; std::atomic bindings; std::atomic evictions; + std::atomic specializationQueries; } @property(nonatomic, strong) NSMutableDictionary* models; diff --git a/backends/apple/coreai/runtime/test/coreai_fake_loader.mm b/backends/apple/coreai/runtime/test/coreai_fake_loader.mm index 7540d60fe74..4edae0b9c71 100644 --- a/backends/apple/coreai/runtime/test/coreai_fake_loader.mm +++ b/backends/apple/coreai/runtime/test/coreai_fake_loader.mm @@ -368,6 +368,7 @@ - (instancetype)init { specializations = 0; bindings = 0; evictions = 0; + specializationQueries = 0; ++_context->state.loaders; } return self; @@ -378,6 +379,7 @@ - (void)dealloc { } } - (NSString*)specializationIdentifier { + ++specializationQueries; return @"default"; } - (void)restoreModelFromBookmark:(NSData*)bookmark From af1e25df208cc16f9a94ee59a6c364efea6bcadf Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Thu, 29 Oct 2026 17:27:58 -0700 Subject: [PATCH 6/7] [CoreAI] Clarify cache ownership and runtime interfaces Make directory ownership and lifecycle responsibilities explicit, support configured symlink directories consistently, and remove ambiguous naming across export and runtime. Consolidate shared helpers and organize the bridge and tests while preserving cache durability and execution contracts. Test Plan: Backend Python suite: 124 passed. Static and shared CoreAI builds succeeded; each configuration passed all 5 CTest targets. One SDK source-lifetime test returned incorrect output during concurrent static/shared runs; the isolated static rerun passed. That unexplained failure is left for separate investigation. Scoped lint, Objective-C++ formatting, and whitespace checks passed. Generic Buck validation was unavailable because its owner query failed without source diagnostics. --- backends/apple/coreai/CMakeLists.txt | 22 +- backends/apple/coreai/README.md | 80 +-- backends/apple/coreai/compiler/preprocess.py | 42 +- .../apple/coreai/runtime/ETCoreAIBridge.h | 44 +- .../apple/coreai/runtime/ETCoreAIError.swift | 30 + ...IModel.swift => ETCoreAIModelLoader.swift} | 229 ------- .../coreai/runtime/ETCoreAITensors.swift | 214 +++++++ .../apple/coreai/runtime/coreai_backend.mm | 48 +- .../apple/coreai/runtime/coreai_bookmarks.h | 105 ---- backends/apple/coreai/runtime/coreai_cache.h | 15 +- backends/apple/coreai/runtime/coreai_cache.mm | 75 +-- .../apple/coreai/runtime/coreai_cache_entry.h | 116 ++++ ...eai_bookmarks.mm => coreai_cache_entry.mm} | 343 +++++------ .../apple/coreai/runtime/coreai_cache_key.h | 10 +- .../apple/coreai/runtime/coreai_cache_key.mm | 33 +- .../apple/coreai/runtime/coreai_cache_root.h | 18 +- .../apple/coreai/runtime/coreai_cache_root.mm | 132 +++- .../coreai/runtime/coreai_fault_injection.h | 16 + backends/apple/coreai/runtime/coreai_file.h | 9 +- .../coreai/runtime/coreai_load_coordinator.h | 16 +- .../coreai/runtime/coreai_load_coordinator.mm | 33 +- .../apple/coreai/runtime/coreai_manifest.h | 10 +- .../apple/coreai/runtime/coreai_manifest.mm | 43 +- .../coreai/runtime/coreai_source_bundle.h | 4 +- .../coreai/runtime/coreai_source_bundle.mm | 58 +- .../apple/coreai/runtime/coreai_storage.h | 10 +- .../apple/coreai/runtime/coreai_storage.mm | 193 +++--- .../apple/coreai/runtime/coreai_validation.h | 14 +- .../apple/coreai/runtime/coreai_validation.mm | 18 +- .../runtime/test/coreai_acquisition_fixture.h | 10 + .../test/coreai_acquisition_fixture.mm | 75 ++- .../runtime/test/coreai_acquisition_test.mm | 22 +- .../test/coreai_bookmark_contention_test.mm | 24 +- .../runtime/test/coreai_bookmark_fixture.h | 11 +- .../runtime/test/coreai_bookmark_fixture.mm | 57 +- .../runtime/test/coreai_bookmark_test.mm | 122 ++-- .../runtime/test/coreai_cache_key_test.mm | 34 +- .../runtime/test/coreai_cache_root_test.mm | 135 ++++- .../coreai/runtime/test/coreai_cache_test.mm | 8 +- .../runtime/test/coreai_delegate_fixture.mm | 9 +- .../runtime/test/coreai_delegate_test.mm | 51 +- .../runtime/test/coreai_eviction_test.mm | 46 +- .../coreai/runtime/test/coreai_fake_loader.mm | 3 +- .../runtime/test/coreai_manifest_fixture.h | 17 +- .../runtime/test/coreai_manifest_test.mm | 67 ++- .../runtime/test/coreai_source_fixture.h | 5 + .../runtime/test/coreai_source_fixture.mm | 24 +- .../test/coreai_source_integrity_test.mm | 303 ++++++++++ .../test/coreai_source_publication_test.mm | 295 +++++++++ .../coreai/runtime/test/coreai_source_test.mm | 567 +----------------- .../runtime/test/coreai_storage_test.mm | 46 +- .../coreai/runtime/test/coreai_test_utils.h | 20 + .../runtime/test/export_bridge_fixture.py | 2 +- .../test/test_export_bridge_fixture.py | 2 +- backends/apple/coreai/test/test_preprocess.py | 8 +- 55 files changed, 2191 insertions(+), 1752 deletions(-) create mode 100644 backends/apple/coreai/runtime/ETCoreAIError.swift rename backends/apple/coreai/runtime/{ETCoreAIModel.swift => ETCoreAIModelLoader.swift} (53%) create mode 100644 backends/apple/coreai/runtime/ETCoreAITensors.swift delete mode 100644 backends/apple/coreai/runtime/coreai_bookmarks.h create mode 100644 backends/apple/coreai/runtime/coreai_cache_entry.h rename backends/apple/coreai/runtime/{coreai_bookmarks.mm => coreai_cache_entry.mm} (57%) create mode 100644 backends/apple/coreai/runtime/test/coreai_source_integrity_test.mm create mode 100644 backends/apple/coreai/runtime/test/coreai_source_publication_test.mm create mode 100644 backends/apple/coreai/runtime/test/coreai_test_utils.h diff --git a/backends/apple/coreai/CMakeLists.txt b/backends/apple/coreai/CMakeLists.txt index a7e70f4af16..ddf41fdbbdd 100644 --- a/backends/apple/coreai/CMakeLists.txt +++ b/backends/apple/coreai/CMakeLists.txt @@ -40,7 +40,7 @@ endif() set(_coreai_runtime_sources runtime/coreai_backend.mm - runtime/coreai_bookmarks.mm + runtime/coreai_cache_entry.mm runtime/coreai_cache.mm runtime/coreai_cache_key.mm runtime/coreai_cache_root.mm @@ -81,6 +81,8 @@ if(EXECUTORCH_BUILD_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Darwin") runtime/test/coreai_bookmark_test.mm runtime/test/coreai_bookmark_contention_test.mm runtime/test/coreai_source_test.mm + runtime/test/coreai_source_integrity_test.mm + runtime/test/coreai_source_publication_test.mm runtime/test/coreai_bookmark_fixture.mm runtime/test/coreai_source_fixture.mm runtime/test/coreai_fake_loader.mm @@ -219,7 +221,11 @@ else() file(MAKE_DIRECTORY "${_coreai_generated_dir}") endif() set(_coreai_swift_header "${_coreai_generated_dir}/CoreAIBridge-Swift.h") -add_library(coreai_swift STATIC runtime/ETCoreAIModel.swift) +set(_coreai_swift_sources + runtime/ETCoreAIError.swift runtime/ETCoreAITensors.swift + runtime/ETCoreAIModelLoader.swift +) +add_library(coreai_swift STATIC ${_coreai_swift_sources}) coreai_configure_swift_target(coreai_swift CoreAIBridge) set_target_properties( coreai_swift PROPERTIES Swift_MODULE_DIRECTORY "${_coreai_generated_dir}" @@ -257,7 +263,7 @@ else() endif() # The bridge header is an input to Swift even when only its declarations change. set_property( - SOURCE runtime/ETCoreAIModel.swift + SOURCE ${_coreai_swift_sources} APPEND PROPERTY OBJECT_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/runtime/ETCoreAIBridge.h" @@ -310,7 +316,7 @@ if(EXECUTORCH_BUILD_TESTS set(_coreai_xctest_libs "${_coreai_platform}/Developer/usr/lib") add_library( coreai_swift_bridge_test MODULE - runtime/ETCoreAIModel.swift runtime/test/SwiftBridgeTests.swift + ${_coreai_swift_sources} runtime/test/SwiftBridgeTests.swift runtime/ETCoreAITensor.mm ) set_target_properties( @@ -367,11 +373,9 @@ if(EXECUTORCH_BUILD_TESTS ) coreai_configure_objc_target(coreai_pte_export_test) target_link_libraries( - coreai_pte_export_test PRIVATE ${_coreai_host_runtime} program_schema + coreai_pte_export_test PRIVATE ${_coreai_runtime} ${_coreai_data_loader} + program_schema ) - if(NOT EXECUTORCH_BUILD_SHARED) - target_link_libraries(coreai_pte_export_test PRIVATE extension_data_loader) - endif() add_dependencies(coreai_pte_export_test coreai_bridge_fixture) if(_coreai_run_sdk_tests) add_test(NAME coreai_pte_export_test @@ -391,7 +395,7 @@ if(EXECUTORCH_BUILD_TESTS endif() add_executable( coreai_sdk_source_lifetime_test - runtime/test/SDKSourceLifetimeTest.swift runtime/ETCoreAIModel.swift + runtime/test/SDKSourceLifetimeTest.swift ${_coreai_swift_sources} runtime/ETCoreAITensor.mm ) coreai_configure_objc_target(coreai_sdk_source_lifetime_test) diff --git a/backends/apple/coreai/README.md b/backends/apple/coreai/README.md index 22828a5a138..eb179d22485 100644 --- a/backends/apple/coreai/README.md +++ b/backends/apple/coreai/README.md @@ -30,15 +30,17 @@ The delegate manifest describes `.aimodel` source bundles, which require `inline` packaging, and AOT-compiled `.aimodelc` bundles, which require `aot_compiled_inline`. Both formats require a `function` name, and ordered `input_names` and `output_names` matching the converter's bindings. Dictionary -iteration order is not a binding contract. The `files` object maps relative +iteration order is not a binding contract. `export_hash` identifies the exported +program used to namespace embedded assets. The `files` object maps relative filenames to byte sizes; `bundle_digests` maps each bundle basename to its export-time SHA-256 digest. Asset paths, including bundle and directory names, must remain distinct under case-insensitive, canonically normalized Unicode comparison. Accepted names are preserved, not rewritten. Parsing returns an immutable `Manifest` covering all exported bundles. Pure -platform and architecture selection produces an immutable `SelectedBundle` -with one bundle's path, digest, and files. Neither type can be constructed +platform and architecture selection through `select_bundle` produces an immutable +`SelectedBundle` with one bundle's path, digest, and files. Host probing returns a +`SelectedHostBundle` containing that selection and its platform/architecture. without validation; filesystem materialization and SDK host probing remain outside the manifest parser. @@ -105,17 +107,18 @@ bookmark restoration. A missing bookmark or SDK-confirmed cache miss falls back to source materialization and persistent specialization, then binds the requested function and ordered I/O on that same acquired model. -### Asset storage +### Cache directory -Backend initialization reads the string runtime spec `coreai_assets_dir`. If +Backend initialization reads the string runtime spec `coreai_cache_dir`. If absent, it sets the directory to `executorch_coreai` beneath user-domain `NSCachesDirectory`. The coordinator and storage helpers always receive that explicit directory. It contains raw bookmarks, staged bundles and lock files. -The assets root must be an absolute path. The application chooses it, should -reserve it for this backend, and is trusted not to rename, replace or modify its -contents while the backend uses it; the backend does not defend against other -processes of the same user rebinding paths. Written files and changed directories +The cache directory must be an absolute path reserved for this backend. A +configured symlink is resolved before opening the cache directory. Entry and +staging operations retain directory descriptors and do not follow internal +symlinks. The application must still prevent concurrent renaming, replacement, +or modification while SDK calls use source URLs. Written files and changed directories are synced with `fsync`. Bookmark publication uses `F_FULLFSYNC` on the temporary file before rename and on the directory afterward. Source-bundle publication fully flushes the staging directory after syncing its files and subdirectories, @@ -126,7 +129,7 @@ volume; unsupported full-device flushes are reported as errors, not downgraded to `fsync`. Inline bundles are read through `NamedDataMap` and materialized as unchanged -files under the assets root. Named data can be supplied externally; inline +files under the cache directory. Named data can be supplied externally; inline packaging does not require all bytes to reside in one physical PTE. The exporter computes each bundle digest from all delivered relative filenames and file bytes, not just the SDK's bytecode-only `main.hash`. Each AOT architecture has an @@ -137,7 +140,7 @@ When source recovery is needed, existing bundles are checked for the expected file set, sizes, entry types and directory structure without reading asset contents or requesting NDS payloads. Unicode paths are compared in canonical decomposed form; NDS keys and bytes remain unchanged. Reuse fully flushes the -source root, including after a previous publication's final flush failed. +staging directory, including after a previous publication's final flush failed. Missing bundles are written to a staging directory and published with an exclusive rename; each selected NDS payload is fetched once and checked for size before writing. If another loader publishes first, its bundle is validated and @@ -156,7 +159,7 @@ NDS source to be reread. The export-time digest is a trusted artifact identifier the runtime does not rehash source or stored bytes. Core AI may reject invalid contents, but SDK load failures do not automatically evict the extracted copy. -Preparing the assets root sets `NSURLIsExcludedFromBackupKey` on it, which covers +Preparing the cache directory sets `NSURLIsExcludedFromBackupKey` on it, which covers everything beneath it. Ancestors are not modified. This is backup exclusion, not a control for iCloud Drive synchronization. @@ -170,7 +173,7 @@ Directory creation uses default permissions filtered by the process umask, witho an explicit owner-only restriction. Existing permissions are not changed. Backup exclusion and required durability flushes still apply on reused paths. -Cache-root preparation owns location and backup/protection policy; descriptor +Cache-directory preparation owns location and backup/protection policy; descriptor storage owns no-follow operations, publication, and synchronization. Creation and enumeration use explicit policies, with a missing child distinguishable from an error. Fault injection is a separate, thread-local test interface and @@ -182,13 +185,15 @@ validating source assets only when recovery requires them. There is no synthetic zero-input inference at initialization. SDK specialization and function resource loading can still be substantial. -The runtime always uses `AIModelCache.default` with persistent retention and -stores an opaque bookmark for direct restoration. `coreai_assets_dir` does not +The runtime uses `AIModelCache.default` with storage-pressure-only purging and +stores an opaque bookmark for direct restoration. `coreai_cache_dir` does not choose the SDK artifact directory. The backend does not inspect SDK-private files, alter their backup flags, or use an App Group cache. -Each selected bundle has a model key derived from its export digest, bundle -name, and source/AOT mode. A readable variant key combines that model key with +Each selected bundle has a `bundle_identity` derived from its bundle digest, +name, and source/AOT mode. This identifies the delivered bundle independently of +`export_hash`, which namespaces embedded assets. A readable cache key combines +that bundle identity with the platform and Core AI device architecture, plus a hash of a versioned specialization identity supplied as plain data by the SDK boundary. Function bindings and delivery location are excluded, and no model bytes are hashed at @@ -200,7 +205,7 @@ A healthy bookmark hit does not consult NDS and attempts to clean leftover staging. On a missing bookmark or confirmed SDK miss, the coordinator prepares the source, specializes it and atomically writes the returned bookmark bytes. After durable publication, it attempts to remove the entire keyed staging -directory while holding the key's stripe lock. Persistent SDK caching supports +directory while holding the key's stripe lock. The SDK cache supports inference and bookmark restoration without that extracted copy. Keep the packaged named data available: a later SDK cache miss requires source materialization again. There is no secondary source-URL cache lookup. @@ -226,7 +231,7 @@ time. Lock ownership is maintained by the OS, and acquiring a lock does not flush its file or directory. Stripe files are never removed, including during cache clearing or after process exit, so waiters and future openers keep using the -same inode. Removing the assets root externally requires all loads, sessions +same inode. Removing the cache directory externally requires all loads, sessions and maintenance to stop. ### Lifecycle logging @@ -239,7 +244,7 @@ multiple partitions can be correlated. These messages do not include bookmark contents, model data, or per-inference logs. A cache hit means that the SDK successfully restored a model from its bookmark. -Specialization start means a request to the SDK's persistent default cache; the +Specialization start means a request to the SDK's default cache; the SDK does not report whether that request compiled afresh or reused an internal artifact. The durable-save message confirms local bookmark publication, including its final directory flush, not independent verification of SDK-private @@ -252,21 +257,21 @@ messages, and clearing an absent entry remains quiet. The public C++ maintenance APIs are in `executorch::backends::coreai`: ```cpp -runtime::Error clear_cache(const char* coreai_assets_dir = nullptr); +runtime::Error clear_cache(const char* coreai_cache_dir = nullptr); runtime::Error clear_cache_for_pte( - runtime::DataLoader& pte, const char* coreai_assets_dir = nullptr); + runtime::DataLoader& pte, const char* coreai_cache_dir = nullptr); runtime::Error clear_cache_for_pte( - const char* pte_path, const char* coreai_assets_dir = nullptr); + const char* pte_path, const char* coreai_cache_dir = nullptr); ``` -All three APIs use the same default assets directory as backend initialization +All three APIs use the same default cache directory as backend initialization when the directory is omitted or null. A supplied path must be absolute; invalid -paths fail without falling back to the default. If loading used a custom root, -pass that same root when clearing; the PTE does not store runtime overrides. +paths fail without falling back to the default. If loading used a custom cache +directory, pass it when clearing; the PTE does not store runtime overrides. -Root-wide clearing inventories keyed bookmarks and staged directories. PTE -clearing finds every Core AI delegate in every method and derives each bundle's -model prefix. Matching that validated prefix clears every recorded platform, +Cache clearing inventories keyed bookmarks and staged directories. PTE clearing +finds every Core AI delegate in every method and derives each bundle's identity. +Matching that identity in cache keys clears every recorded platform, device, and specialization variant without querying the host or SDK. All manifests are validated before any entry is cleared; their minimum OS and runtime compatibility do not restrict maintenance. Discovery does not load models or @@ -303,8 +308,8 @@ SDK errors are not classified as transient or permanent. Busy errors and all unclassified eviction/restore errors retain the local bookmark for retry; only successful eviction or a confirmed SDK miss permits normal removal. If retries keep failing, first stop every process using the -assets root. Back up or quarantine only the affected key's bookmark and keyed -staging outside that root, leaving the stable lock files in place. A later load +cache directory. Back up or quarantine only the affected key's bookmark and keyed +staging outside that directory, leaving the stable lock files in place. A later load can attempt cold acquisition and publish a new bookmark. This manual recovery does not confirm SDK eviction or guarantee disk-space reclamation: keep the backup because it may contain the only remaining eviction @@ -318,25 +323,26 @@ loader using the `executorch::runtime` namespace: ```cpp BackendOptions<1> options; -ET_CHECK_OK_OR_RETURN_ERROR(options.set_option("coreai_assets_dir", assets_dir)); +ET_CHECK_OK_OR_RETURN_ERROR(options.set_option("coreai_cache_dir", cache_dir)); LoadBackendOptionsMap map; ET_CHECK_OK_OR_RETURN_ERROR(map.set_options("CoreAIBackend", options.view())); ET_CHECK_OK_OR_RETURN_ERROR(module.load(map)); ``` -No options are required when using the default assets root. `coreai_assets_dir` +No options are required when using the default cache directory. `coreai_cache_dir` selects an application-chosen cache directory reserved for this backend. The supplied path is validated during preflight, but warm bookmark hits need no source. -Use separate option maps to choose different storage roots for different models. -The path is a root directory, not the bundle itself. ExecuTorch option strings +Use separate option maps to choose different cache directories for different models. +The path names the cache directory, not a bundle. ExecuTorch option strings have a 255-byte limit. ## SDK Bridge -The private Swift module `CoreAIBridge` (`runtime/ETCoreAIModel.swift`) wraps -the `CoreAI` framework behind the Objective-C protocols in +The private Swift module `CoreAIBridge` separates error/completion support, +tensor layout/copying, and model/session lifecycle into three source files. It +wraps the `CoreAI` framework behind the Objective-C protocols in `runtime/ETCoreAIBridge.h`. It restores models from bookmarks, specializes source bundles in the default SDK cache with storage-pressure-only purging, and binds named functions with ordered inputs and outputs. Each loader owns an immutable diff --git a/backends/apple/coreai/compiler/preprocess.py b/backends/apple/coreai/compiler/preprocess.py index cf2f86224a8..7ff320accb1 100644 --- a/backends/apple/coreai/compiler/preprocess.py +++ b/backends/apple/coreai/compiler/preprocess.py @@ -157,10 +157,9 @@ def _get_compile_spec( return None -def _nds_key(model_hash: str, relative_path: str) -> str: - # Namespaced by content hash so identical assets dedup to one entry and the - # hash doubles as an on-device cache key. - return f"coreai/{model_hash}/{relative_path}" +def _nds_key(export_hash: str, relative_path: str) -> str: + # Identical exports share embedded assets under their export hash. + return f"coreai/{export_hash}/{relative_path}" def _asset_metadata(staging: Path) -> Dict[str, Any]: @@ -194,23 +193,23 @@ def _asset_metadata(staging: Path) -> Dict[str, Any]: def _deliver( staging: Path, - model_hash: str, + export_hash: str, packaging: AssetPackaging, manifest_extra: Dict[str, Any], ) -> PreprocessResult: """Embed a staging directory of bundles in the .pte with its manifest. ``staging`` holds the ``.aimodel`` / ``.aimodelc`` bundles, never their - contents, so assets land under ``coreai///`` in the + contents, so assets land under ``coreai///`` in the NamedDataStore. """ manifest = { "packaging": packaging.value, - "hash": model_hash, + "export_hash": export_hash, **manifest_extra, **_asset_metadata(staging), } - return _embed_dir_inline(staging, model_hash, manifest) + return _embed_dir_inline(staging, export_hash, manifest) def _prepare_program_for_conversion(edge_program: ExportedProgram) -> ExportedProgram: @@ -255,21 +254,21 @@ def _convert_to_aiprogram( # Asset embedding helpers. def _embed_dir_inline( - root_dir: Path, model_hash: str, manifest_extra: Dict[str, Any] + root_dir: Path, export_hash: str, manifest_extra: Dict[str, Any] ) -> PreprocessResult: """Flatten every file under ``root_dir`` into the NamedDataStore. ``root_dir`` is the directory *containing* the ``.aimodel`` / ``.aimodelc`` bundles, never a bundle itself, so the bundle name survives into the keys. - Keys are ``coreai/{hash}/{relpath}``; ``manifest_extra`` becomes the + Keys are ``coreai/{export_hash}/{relpath}``; ``manifest_extra`` becomes the manifest (packaging, files, archs, ...). """ store = NamedDataStore() for path in sorted(root_dir.rglob("*")): if path.is_file(): store.add_named_data( - _nds_key(model_hash, path.relative_to(root_dir).as_posix()), + _nds_key(export_hash, path.relative_to(root_dir).as_posix()), path.read_bytes(), alignment=_ASSET_ALIGNMENT, ) @@ -426,15 +425,15 @@ def _compiled_arch_bundles(out_dir: Path) -> List[Tuple[str, Path]]: def _compile_aot(program, opts: Dict[str, Any], tmp_dir: Path) -> Tuple[str, Path]: """Save the intermediate ``.aimodel`` and AOT-compile it to ``.aimodelc``. - Returns ``(model_hash, compiled_out_dir)``. The intermediate ``.aimodel`` + Returns ``(export_hash, compiled_out_dir)``. The intermediate ``.aimodel`` keeps the default OS floor; ``min_deployment_version`` targets the compiled ``.aimodelc`` via ``coreai-build``, so it is not double-specified. """ - model_hash = _save_and_hash(program, tmp_dir / "model.aimodel") + export_hash = _save_and_hash(program, tmp_dir / "model.aimodel") out = tmp_dir / "compiled" out.mkdir() _run_coreai_build(tmp_dir / "model.aimodel", out, opts) - return model_hash, out + return export_hash, out @final @@ -446,8 +445,7 @@ class CoreAIBackend(BackendDetails): ``xcrun coreai-build``; architecture selection is one / a list / all). The bundles are embedded in the ``.pte`` via NamedDataStore. - ``processed_bytes`` is always a small JSON manifest naming what/where. - Runtime execution is not wired up yet. + ``processed_bytes`` contains the manifest used by the runtime delegate. """ @staticmethod @@ -514,15 +512,15 @@ def _preprocess_portable( program, min_os, bindings: Dict[str, Any] ) -> PreprocessResult: with TemporaryDirectory() as tmp: - model_hash = _save_and_hash(program, Path(tmp) / "model.aimodel", min_os) + export_hash = _save_and_hash(program, Path(tmp) / "model.aimodel", min_os) return _deliver( Path(tmp), - model_hash, + export_hash, AssetPackaging.INLINE, { **bindings, # relative path the runtime resolves against its base - "path": f"{model_hash}/model.aimodel", + "path": f"{export_hash}/model.aimodel", "min_deployment_version": _os_version_text(min_os), }, ) @@ -533,17 +531,17 @@ def _preprocess_aot_compiled( program, opts: Dict[str, Any], bindings: Dict[str, Any] ) -> PreprocessResult: with TemporaryDirectory() as tmp: - model_hash, out = _compile_aot(program, opts, Path(tmp)) + export_hash, out = _compile_aot(program, opts, Path(tmp)) return _deliver( out, - model_hash, + export_hash, AssetPackaging.AOT_COMPILED_INLINE, { **bindings, "platform": opts["platform"], "min_deployment_version": opts["min_deployment_version"], "archs": { - arch: f"{model_hash}/{bundle.name}" + arch: f"{export_hash}/{bundle.name}" for arch, bundle in _compiled_arch_bundles(out) }, }, diff --git a/backends/apple/coreai/runtime/ETCoreAIBridge.h b/backends/apple/coreai/runtime/ETCoreAIBridge.h index 73426ac00c6..c16f2cae5a9 100644 --- a/backends/apple/coreai/runtime/ETCoreAIBridge.h +++ b/backends/apple/coreai/runtime/ETCoreAIBridge.h @@ -33,7 +33,8 @@ typedef NS_ENUM(NSInteger, ETCoreAIErrorCode) { FOUNDATION_EXPORT NSErrorDomain const ETCoreAIErrorDomain; -// Input storage is borrowed through output copying, never retained or freed. +// Input storage is borrowed until output copying finishes. The bridge never +// retains or frees it. @interface ETCoreAIInputTensor : NSObject @property(nonatomic, readonly) const void* _Nullable bytes; @property(nonatomic, readonly) NSUInteger byteCount; @@ -48,9 +49,8 @@ FOUNDATION_EXPORT NSErrorDomain const ETCoreAIErrorDomain; NS_DESIGNATED_INITIALIZER; @end -// The copier retains its source storage; SDK pointers never escape their access -// scope. Construct only after validating the source layout. Copying cannot -// fail. +// The copier retains its source storage; SDK pointers stay within their access +// scope. Validate the source layout before constructing. Copying cannot fail. @interface ETCoreAITensor : NSObject @property(nonatomic, readonly) NSUInteger byteCount; @property(nonatomic, readonly, copy) NSArray* shape; @@ -70,11 +70,12 @@ FOUNDATION_EXPORT NSErrorDomain const ETCoreAIErrorDomain; - (void)copyBytesTo:(void* _Nullable)bytes NS_SWIFT_NAME(copyBytes(to:)); @end -// The caller keeps inputs valid until all output copies finish, or completion -// reports failure. Outputs may share input storage; stage copies before writing -// any destination that aliases an input or another output destination. -// Completion is called exactly once, possibly before executeInputs returns, -// with no guarantee of a particular thread or queue. +// Keep inputs valid until output copying finishes or completion reports +// failure. Outputs may share input storage. Stage copies before writing any +// destination that aliases an input or another output destination. +// +// Completion runs exactly once, possibly before executeInputs returns, on an +// unspecified thread or queue. @protocol ETCoreAISession - (void)executeInputs:(NSArray*)inputs completion:(void (^)( @@ -82,12 +83,12 @@ FOUNDATION_EXPORT NSErrorDomain const ETCoreAIErrorDomain; NSError* _Nullable error))completion; @end -// A prepared model pins the SDK entry. Sessions retain their own pin after +// A prepared model pins the SDK entry. Each session retains its own pin after // binding. @protocol ETCoreAIPreparedModel - (NSData*)copyBookmarkData; -// Completes once with either a session or an error, never both. May complete -// synchronously on the calling thread. +// Completes once with either a session or an error, never both. Completion may +// run synchronously on the calling thread. - (void)loadFunctionNamed:(NSString*)functionName inputNames:(NSArray*)inputNames outputNames:(NSArray*)outputNames @@ -102,26 +103,27 @@ typedef NS_ENUM(NSInteger, ETCoreAIAcquisitionStatus) { ETCoreAIAcquisitionStatusError = 2, }; -// Hit: nonnull model, nil error. Miss: nil model and error, only on SDK nil. -// Error: nil model, nonnull error. Function errors never produce a Miss. -// Acquisition operations call completion exactly once off the main thread. +// Hit: nonnull model, nil error. +// Miss: nil model and error, only for an SDK nil result. +// Error: nil model, nonnull error. Function errors never report Miss. +// +// Acquisition completions run exactly once off the main thread. typedef void (^ETCoreAIAcquisitionCompletion)( ETCoreAIAcquisitionStatus status, id _Nullable preparedModel, NSError* _Nullable error); @protocol ETCoreAIModelLoading -// Stable for this loader's lifetime; derived from its actual specialization -// options. +// Stable for this loader's lifetime; derived from its specialization options. @property(nonatomic, readonly, copy) NSString* specializationIdentifier; - (void)restoreModelFromBookmark:(NSData*)bookmark completion:(ETCoreAIAcquisitionCompletion)completion; -// Uses the SDK default cache with storage-pressure-only purging. Never returns -// Miss. +// Uses the SDK default cache with storage-pressure-only purging. +// Specialization never reports Miss. - (void)specializeModelAtURL:(NSURL*)url completion:(ETCoreAIAcquisitionCompletion)completion; -// Nil error means SDK-confirmed deletion. No busy/absent classification is -// inferred. +// Nil error means SDK-confirmed deletion; the bridge adds no +// busy/absent classification. - (void)evictModelWithBookmark:(NSData*)bookmark completion:(void (^)(NSError* _Nullable error))completion; @end diff --git a/backends/apple/coreai/runtime/ETCoreAIError.swift b/backends/apple/coreai/runtime/ETCoreAIError.swift new file mode 100644 index 00000000000..65c20c5f472 --- /dev/null +++ b/backends/apple/coreai/runtime/ETCoreAIError.swift @@ -0,0 +1,30 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// All rights reserved. +// +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. + +import Foundation + +func bridgeError( + _ code: ETCoreAIErrorCode, _ message: String, underlying: Error? = nil +) -> NSError { + var info: [String: Any] = [NSLocalizedDescriptionKey: message] + if let underlying { info[NSUnderlyingErrorKey] = underlying } + return NSError(domain: ETCoreAIErrorDomain, code: code.rawValue, userInfo: info) +} + +func mappedError(_ error: Error, code: ETCoreAIErrorCode, context: String) -> NSError { + let error = error as NSError + return error.domain == ETCoreAIErrorDomain + ? error : bridgeError(code, "\(context): \(error.localizedDescription)", underlying: error) +} + +// Objective-C blocks have no Sendable annotation. Each box is invoked by one detached task only. +final class Completion: @unchecked Sendable { + let call: Callback + + init(_ call: Callback) { + self.call = call + } +} diff --git a/backends/apple/coreai/runtime/ETCoreAIModel.swift b/backends/apple/coreai/runtime/ETCoreAIModelLoader.swift similarity index 53% rename from backends/apple/coreai/runtime/ETCoreAIModel.swift rename to backends/apple/coreai/runtime/ETCoreAIModelLoader.swift index 1e7dc71fffb..adf80fbc91f 100644 --- a/backends/apple/coreai/runtime/ETCoreAIModel.swift +++ b/backends/apple/coreai/runtime/ETCoreAIModelLoader.swift @@ -7,29 +7,6 @@ import CoreAI import Foundation -private func bridgeError( - _ code: ETCoreAIErrorCode, _ message: String, underlying: Error? = nil -) -> NSError { - var info: [String: Any] = [NSLocalizedDescriptionKey: message] - if let underlying { info[NSUnderlyingErrorKey] = underlying } - return NSError(domain: ETCoreAIErrorDomain, code: code.rawValue, userInfo: info) -} - -private func mappedError(_ error: Error, code: ETCoreAIErrorCode, context: String) -> NSError { - let error = error as NSError - return error.domain == ETCoreAIErrorDomain - ? error : bridgeError(code, "\(context): \(error.localizedDescription)", underlying: error) -} - -// Objective-C blocks have no Sendable annotation. Each box is invoked by one detached task only. -private final class Completion: @unchecked Sendable { - let call: Callback - - init(_ call: Callback) { - self.call = call - } -} - // Only a successful SDK nil is a miss; thrown errors retain their underlying cause. func acquireCoreAIModel( context: String, completion: @escaping ETCoreAIAcquisitionCompletion, @@ -49,212 +26,6 @@ func acquireCoreAIModel( } } -@available(macOS 27.0, iOS 27.0, *) -struct CoreAIScalarInfo: Sendable { - let sdkType: NDArray.ScalarType - let bridgeType: ETCoreAIScalarType - let byteWidth: Int - - // Only byte-addressable scalar types belong here; packed types need a separate layout policy. - static let supported: [Self] = [ - Self(sdkType: .float16, bridgeType: .float16, byteWidth: 2), - Self(sdkType: .float32, bridgeType: .float32, byteWidth: 4), - ] - - static func from(sdkType: NDArray.ScalarType) throws -> Self { - guard let info = supported.first(where: { $0.sdkType == sdkType }) else { - throw bridgeError(.unsupported, "Unsupported Core AI scalar type: \(sdkType)") - } - return info - } - - static func from(bridgeType: ETCoreAIScalarType) throws -> Self { - guard let info = supported.first(where: { $0.bridgeType == bridgeType }) else { - throw bridgeError(.unsupported, "Unsupported bridge scalar type: \(bridgeType.rawValue)") - } - return info - } - - func byteCount(shape: [Int], errorCode: ETCoreAIErrorCode = .invalidArgument) throws -> Int { - guard shape.allSatisfy({ $0 >= 0 }) else { - throw bridgeError(errorCode, "Negative tensor dimension") - } - // Check suffix products too: SDK dense-stride construction must not overflow on empty tensors. - var count = byteWidth - for dimension in shape.reversed() { - let (next, overflow) = count.multipliedReportingOverflow(by: max(dimension, 1)) - guard !overflow else { - throw bridgeError(errorCode, "Tensor byte count overflows Int") - } - count = next - } - return shape.contains(0) ? 0 : count - } -} - -// ExecuTorch waits synchronously and keeps input storage live through output copying. -// This snapshot transfers only metadata and a pointer to the task; it does not own storage. -@available(macOS 27.0, iOS 27.0, *) -struct CoreAITensorPayload: @unchecked Sendable { - let bytes: UnsafeRawPointer? - let byteCount: Int - let shape: [Int] - let scalarType: ETCoreAIScalarType - - init(_ tensor: ETCoreAIInputTensor) throws { - shape = try tensor.shape.map { number in - guard let dimension = number as? Int, dimension >= 0 else { - throw bridgeError(.invalidArgument, "Tensor dimensions must be nonnegative integers") - } - return dimension - } - scalarType = tensor.scalarType - byteCount = try Self.byteCount(shape: shape, scalarType: scalarType) - guard tensor.byteCount == byteCount else { - throw bridgeError( - .invalidArgument, "Tensor payload byte count does not match shape and dtype") - } - bytes = tensor.bytes - guard byteCount == 0 || bytes != nil else { - throw bridgeError(.invalidArgument, "Nonempty tensor has null data") - } - } - - static func byteCount( - shape: [Int], scalarType: ETCoreAIScalarType, - errorCode: ETCoreAIErrorCode = .invalidArgument - ) throws -> Int { - try CoreAIScalarInfo.from(bridgeType: scalarType).byteCount(shape: shape, errorCode: errorCode) - } -} - -@available(macOS 27.0, iOS 27.0, *) -private func tensorDescriptor(_ value: InferenceValue.Descriptor?) throws -> NDArrayDescriptor { - guard let value else { throw bridgeError(.invalidModel, "Missing tensor descriptor") } - guard case .ndArray(let descriptor) = value else { - throw bridgeError(.unsupported, "Image values are not supported") - } - _ = try CoreAIScalarInfo.from(sdkType: descriptor.scalarType) - guard descriptor.interleaveLayout == nil else { - throw bridgeError(.unsupported, "Interleaved tensor descriptors are not supported") - } - guard descriptor.shape.allSatisfy({ $0 >= -1 }) else { - throw bridgeError(.invalidModel, "Invalid descriptor dimension") - } - return descriptor -} - -@available(macOS 27.0, iOS 27.0, *) -private func validateShape( - _ shape: [Int], descriptor: NDArrayDescriptor, - errorCode: ETCoreAIErrorCode = .invalidArgument -) throws { - guard shape.count == descriptor.shape.count, - zip(shape, descriptor.shape).allSatisfy({ actual, expected in - actual >= 0 && (expected == -1 || actual == expected) - }) - else { - throw bridgeError(errorCode, "Tensor shape does not match the function descriptor") - } -} - -// Offsets are in elements, including padded or transposed SDK layouts. -struct CoreAITensorLayout { - let shape: [Int] - let strides: [Int] - let count: Int - // Row-major contiguous, ignoring strides of size-1 dimensions. - let isDense: Bool - - init(shape: [Int], strides: [Int], width: Int, storageByteCount: Int) throws { - guard shape.count == strides.count, shape.allSatisfy({ $0 >= 0 }), - strides.allSatisfy({ $0 >= 0 }), width > 0, storageByteCount >= 0 - else { throw bridgeError(.unsupported, "Invalid or negative NDArray strides") } - self.shape = shape - self.strides = strides - var count = 1 - var maximumOffset = 0 - for (dimension, stride) in zip(shape, strides) { - let (next, countOverflow) = count.multipliedReportingOverflow(by: dimension) - let (extent, extentOverflow) = max(dimension - 1, 0).multipliedReportingOverflow(by: stride) - let (offset, offsetOverflow) = maximumOffset.addingReportingOverflow(extent) - guard !countOverflow, !extentOverflow, !offsetOverflow else { - throw bridgeError(.runtime, "NDArray layout overflows Int") - } - count = next - maximumOffset = offset - } - self.count = count - var expectedStride = 1 - var isDense = true - for axis in shape.indices.reversed() where shape[axis] != 1 { - isDense = isDense && strides[axis] == expectedStride - expectedStride = expectedStride &* shape[axis] - } - self.isDense = isDense - if count > 0 { - let (span, spanOverflow) = maximumOffset.addingReportingOverflow(1) - let (bytes, byteOverflow) = span.multipliedReportingOverflow(by: width) - guard !spanOverflow, !byteOverflow, bytes <= storageByteCount else { - throw bridgeError(.runtime, "NDArray layout exceeds its storage") - } - } - } - - func offset(for linearIndex: Int) -> Int { - var remainder = linearIndex - var offset = 0 - for axis in shape.indices.reversed() { - offset += (remainder % shape[axis]) * strides[axis] - remainder /= shape[axis] - } - return offset - } -} - -@available(macOS 27.0, iOS 27.0, *) -func prepareOutput(_ array: NDArray) throws -> ETCoreAITensor { - let type = try CoreAIScalarInfo.from(sdkType: array.scalarType) - let byteCount = try type.byteCount(shape: array.shape, errorCode: .runtime) - guard array.interleaveLayout == nil else { - throw bridgeError(.unsupported, "Interleaved output storage is not supported") - } - let view = array.rawView() - let layout = try CoreAITensorLayout( - shape: array.shape, strides: array.strides, width: type.byteWidth, - storageByteCount: view.bytes.byteCount) - let shape = view.shape - let strides = view.strides - guard view.scalarType == type.sdkType, view.interleaveLayout == nil, - shape.count == layout.shape.count, strides.count == layout.strides.count - else { - throw bridgeError(.runtime, "Output view layout changed") - } - for axis in layout.shape.indices { - guard shape[axis] == layout.shape[axis], strides[axis] == layout.strides[axis] else { - throw bridgeError(.runtime, "Output view layout changed") - } - } - // Retain the immutable array, not its scoped pointer. All fallible work is complete. - return ETCoreAITensor( - byteCount: UInt(byteCount), shape: layout.shape.map(NSNumber.init(value:)), - scalarType: type.bridgeType - ) { destination in - let view = array.rawView() - view.bytes.withUnsafeBytes { source in - if layout.isDense { - destination.copyMemory(from: source.baseAddress!, byteCount: byteCount) - return - } - for index in 0.. Self { + guard let info = supported.first(where: { $0.sdkType == sdkType }) else { + throw bridgeError(.unsupported, "Unsupported Core AI scalar type: \(sdkType)") + } + return info + } + + static func from(bridgeType: ETCoreAIScalarType) throws -> Self { + guard let info = supported.first(where: { $0.bridgeType == bridgeType }) else { + throw bridgeError(.unsupported, "Unsupported bridge scalar type: \(bridgeType.rawValue)") + } + return info + } + + func byteCount(shape: [Int], errorCode: ETCoreAIErrorCode = .invalidArgument) throws -> Int { + guard shape.allSatisfy({ $0 >= 0 }) else { + throw bridgeError(errorCode, "Negative tensor dimension") + } + // Check suffix products too: SDK dense-stride construction must not overflow on empty tensors. + var count = byteWidth + for dimension in shape.reversed() { + let (next, overflow) = count.multipliedReportingOverflow(by: max(dimension, 1)) + guard !overflow else { + throw bridgeError(errorCode, "Tensor byte count overflows Int") + } + count = next + } + return shape.contains(0) ? 0 : count + } +} + +// ExecuTorch waits synchronously and keeps input storage live through output copying. +// This snapshot transfers only metadata and a pointer to the task; it does not own storage. +@available(macOS 27.0, iOS 27.0, *) +struct CoreAITensorPayload: @unchecked Sendable { + let bytes: UnsafeRawPointer? + let byteCount: Int + let shape: [Int] + let scalarType: ETCoreAIScalarType + + init(_ tensor: ETCoreAIInputTensor) throws { + shape = try tensor.shape.map { number in + guard let dimension = number as? Int, dimension >= 0 else { + throw bridgeError(.invalidArgument, "Tensor dimensions must be nonnegative integers") + } + return dimension + } + scalarType = tensor.scalarType + byteCount = try Self.byteCount(shape: shape, scalarType: scalarType) + guard tensor.byteCount == byteCount else { + throw bridgeError( + .invalidArgument, "Tensor payload byte count does not match shape and dtype") + } + bytes = tensor.bytes + guard byteCount == 0 || bytes != nil else { + throw bridgeError(.invalidArgument, "Nonempty tensor has null data") + } + } + + static func byteCount( + shape: [Int], scalarType: ETCoreAIScalarType, + errorCode: ETCoreAIErrorCode = .invalidArgument + ) throws -> Int { + try CoreAIScalarInfo.from(bridgeType: scalarType).byteCount(shape: shape, errorCode: errorCode) + } +} + +@available(macOS 27.0, iOS 27.0, *) +func tensorDescriptor(_ value: InferenceValue.Descriptor?) throws -> NDArrayDescriptor { + guard let value else { throw bridgeError(.invalidModel, "Missing tensor descriptor") } + guard case .ndArray(let descriptor) = value else { + throw bridgeError(.unsupported, "Image values are not supported") + } + _ = try CoreAIScalarInfo.from(sdkType: descriptor.scalarType) + guard descriptor.interleaveLayout == nil else { + throw bridgeError(.unsupported, "Interleaved tensor descriptors are not supported") + } + guard descriptor.shape.allSatisfy({ $0 >= -1 }) else { + throw bridgeError(.invalidModel, "Invalid descriptor dimension") + } + return descriptor +} + +@available(macOS 27.0, iOS 27.0, *) +func validateShape( + _ shape: [Int], descriptor: NDArrayDescriptor, + errorCode: ETCoreAIErrorCode = .invalidArgument +) throws { + guard shape.count == descriptor.shape.count, + zip(shape, descriptor.shape).allSatisfy({ actual, expected in + actual >= 0 && (expected == -1 || actual == expected) + }) + else { + throw bridgeError(errorCode, "Tensor shape does not match the function descriptor") + } +} + +// Offsets are in elements, including padded or transposed SDK layouts. +struct CoreAITensorLayout { + let shape: [Int] + let strides: [Int] + let count: Int + // Row-major contiguous, ignoring strides of size-1 dimensions. + let isDense: Bool + + init(shape: [Int], strides: [Int], width: Int, storageByteCount: Int) throws { + guard shape.count == strides.count, shape.allSatisfy({ $0 >= 0 }), + strides.allSatisfy({ $0 >= 0 }), width > 0, storageByteCount >= 0 + else { throw bridgeError(.unsupported, "Invalid or negative NDArray strides") } + self.shape = shape + self.strides = strides + var count = 1 + var maximumOffset = 0 + for (dimension, stride) in zip(shape, strides) { + let (next, countOverflow) = count.multipliedReportingOverflow(by: dimension) + let (extent, extentOverflow) = max(dimension - 1, 0).multipliedReportingOverflow(by: stride) + let (offset, offsetOverflow) = maximumOffset.addingReportingOverflow(extent) + guard !countOverflow, !extentOverflow, !offsetOverflow else { + throw bridgeError(.runtime, "NDArray layout overflows Int") + } + count = next + maximumOffset = offset + } + self.count = count + var expectedStride = 1 + var isDense = true + for axis in shape.indices.reversed() where shape[axis] != 1 { + isDense = isDense && strides[axis] == expectedStride + expectedStride = expectedStride &* shape[axis] + } + self.isDense = isDense + if count > 0 { + let (span, spanOverflow) = maximumOffset.addingReportingOverflow(1) + let (bytes, byteOverflow) = span.multipliedReportingOverflow(by: width) + guard !spanOverflow, !byteOverflow, bytes <= storageByteCount else { + throw bridgeError(.runtime, "NDArray layout exceeds its storage") + } + } + } + + func offset(for linearIndex: Int) -> Int { + var remainder = linearIndex + var offset = 0 + for axis in shape.indices.reversed() { + offset += (remainder % shape[axis]) * strides[axis] + remainder /= shape[axis] + } + return offset + } +} + +@available(macOS 27.0, iOS 27.0, *) +func prepareOutput(_ array: NDArray) throws -> ETCoreAITensor { + let type = try CoreAIScalarInfo.from(sdkType: array.scalarType) + let byteCount = try type.byteCount(shape: array.shape, errorCode: .runtime) + guard array.interleaveLayout == nil else { + throw bridgeError(.unsupported, "Interleaved output storage is not supported") + } + let view = array.rawView() + let layout = try CoreAITensorLayout( + shape: array.shape, strides: array.strides, width: type.byteWidth, + storageByteCount: view.bytes.byteCount) + let shape = view.shape + let strides = view.strides + guard view.scalarType == type.sdkType, view.interleaveLayout == nil, + shape.count == layout.shape.count, strides.count == layout.strides.count + else { + throw bridgeError(.runtime, "Output view layout changed") + } + for axis in layout.shape.indices { + guard shape[axis] == layout.shape[axis], strides[axis] == layout.strides[axis] else { + throw bridgeError(.runtime, "Output view layout changed") + } + } + // Retain the immutable array, not its scoped pointer. All fallible work is complete. + return ETCoreAITensor( + byteCount: UInt(byteCount), shape: layout.shape.map(NSNumber.init(value:)), + scalarType: type.bridgeType + ) { destination in + let view = array.rawView() + view.bytes.withUnsafeBytes { source in + if layout.isDense { + destination.copyMemory(from: source.baseAddress!, byteCount: byteCount) + return + } + for index in 0.. session = nil; }; -Result path_option(BackendInitContext& context, const char* key) { - auto option = context.get_runtime_spec(key); +Result cache_directory_option(BackendInitContext& context) { + auto option = context.get_runtime_spec("coreai_cache_dir"); if (!option.ok()) { if (option.error() == Error::NotFound) { - return static_cast(nil); + return cache_directory_path(nullptr); } return option.error(); } - NSString* path = option.get() == nullptr - ? nil - : [NSString stringWithUTF8String:option.get()]; ET_CHECK_OR_RETURN_ERROR( - path != nil && path.isAbsolutePath, + option.get() != nullptr, InvalidArgument, - "Core AI %s must be an absolute UTF-8 path", - key); - return path; + "Core AI cache directory option must not be null"); + return cache_directory_path(option.get()); } Result> bind_session( @@ -93,27 +89,31 @@ bool is_available() const override { if (!manifest.ok()) { return manifest.error(); } - auto assets = select_runtime_assets(manifest.get()); + auto assets = select_host_bundle(manifest.get()); if (!assets.ok()) { return assets.error(); } - auto assets_option = path_option(context, "coreai_assets_dir"); - if (!assets_option.ok()) { - return assets_option.error(); - } - NSString* root = assets_option.get(); - if (root == nil) { - auto default_root = default_coreai_assets_root(); - if (!default_root.ok()) { - return default_root.error(); - } - root = default_root.get(); + auto configured = cache_directory_option(context); + if (!configured.ok()) { + return configured.error(); } id loader = ETCoreAICreateModelLoader(); ET_CHECK_OR_RETURN_ERROR( loader != nil, Internal, "Cannot create Core AI loader"); - auto prepared = acquire_bookmark_model( - assets.get(), context.get_named_data_map(), root, loader); + auto cache_directory = resolve_cache_directory( + configured.get(), StorageCreationMode::Create); + if (!cache_directory.ok()) { + return cache_directory.error(); + } + ET_CHECK_OR_RETURN_ERROR( + cache_directory->has_value(), + AccessFailed, + "Core AI cache directory disappeared"); + auto prepared = acquire_model( + assets.get(), + context.get_named_data_map(), + cache_directory->value(), + loader); if (!prepared.ok()) { return prepared.error(); } diff --git a/backends/apple/coreai/runtime/coreai_bookmarks.h b/backends/apple/coreai/runtime/coreai_bookmarks.h deleted file mode 100644 index 6646c31803d..00000000000 --- a/backends/apple/coreai/runtime/coreai_bookmarks.h +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ -#pragma once - -#import -#include -#include -#include "coreai_file.h" - -namespace executorch::backends::coreai { - -// With OpenExisting, a missing root is returned unchanged for no-op -// maintenance. -runtime::Result resolve_bookmark_root( - NSString* path, - StorageCreationMode mode); -// Missing roots produce an empty inventory. Invalid or unowned names are -// ignored. -runtime::Result*> inventory_cache_keys(NSString* root); - -enum class CacheBookmarkState { - Missing, - Corrupt, - Present, -}; - -struct CacheBookmarkInspection final { - CacheBookmarkState state; - NSData* data; -}; - -enum class CacheEntryPersistence { - BookmarkCapable, - StagingOnly, -}; - -class CacheEntryLock final { - public: - CacheEntryLock(const CacheEntryLock&) = delete; - CacheEntryLock& operator=(const CacheEntryLock&) = delete; - - CacheEntryPersistence persistence() const { - return persistence_; - } - NSString* key() const { - return key_; - } - // Missing is nil. Empty, oversized, inaccessible, or nonregular files fail - // closed. - runtime::Result read_bookmark() const; - runtime::Error write_bookmark(NSData* data) const; - // Returns the staging parent. Source preparation appends key and bundle. - runtime::Result prepare_staging_root() const; - // Missing staging is a no-op; unexpected entry types fail before deletion. - runtime::Error remove_staging() const; - - // Maintenance accepts malformed backend-owned entries but never follows - // them. The Strict runtime cleanup above is intentionally separate. - runtime::Result inspect_bookmark_for_maintenance() - const; - runtime::Error preflight_maintenance() const; - runtime::Error remove_staging_for_maintenance() const; - runtime::Error remove_bookmark_for_maintenance() const; - - private: - friend runtime::Result> - acquire_cache_entry_lock( - NSString* root, - NSString* key, - StorageCreationMode mode); - - CacheEntryLock( - int root, - int bookmarks, - int lock_file, - NSString* root_path, - NSString* key, - CacheEntryPersistence persistence); - - FileDescriptor root_; - FileDescriptor bookmarks_; - FileDescriptor lock_file_; - NSString* root_path_; - NSString* key_; - CacheEntryPersistence persistence_; -}; - -// Each acquisition opens an independent descriptor on one stable stripe. -// Different keys can share a stripe; callers must not hold multiple entry locks -// at once. Stripe files are never removed by the backend. -runtime::Result> lock_cache_entry( - NSString* root, - NSString* key); -// Missing roots and entries return a null lock without changing storage. -// Existing staging-only entries return a lock with StagingOnly persistence. -runtime::Result> lock_existing_cache_entry( - NSString* root, - NSString* key); - -} // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_cache.h b/backends/apple/coreai/runtime/coreai_cache.h index 94e14968bc7..f542910d31d 100644 --- a/backends/apple/coreai/runtime/coreai_cache.h +++ b/backends/apple/coreai/runtime/coreai_cache.h @@ -17,8 +17,8 @@ namespace executorch::backends::coreai { /** * Clears tracked SDK entries, keyed staging, and bookmarks in a dedicated - * Core AI assets directory. Null selects the same default as loading; supplied - * paths must be absolute. Missing roots are successful no-ops. + * Core AI cache directory. Null selects the same default as loading; supplied + * paths must be absolute. Missing directories are successful no-ops. * PTEs, stable lock files, and unkeyed temporaries are not removed. * SDK entries without saved bookmarks cannot be targeted. * @@ -29,7 +29,7 @@ namespace executorch::backends::coreai { * the SDK confirms that entry is absent. */ [[nodiscard]] runtime::Error clear_cache( - const char* coreai_assets_dir = nullptr); + const char* coreai_cache_dir = nullptr); /** * Clears all recorded cache variants referenced by every CoreAI delegate in @@ -39,19 +39,20 @@ namespace executorch::backends::coreai { * delegates or materialize Core AI assets. * * A null directory selects the same Caches default as backend initialization. - * Supplied directories must be valid explicit roots; they never fall back to - * the default. The directory is never derived from the PTE's location. + * Supplied paths must resolve to valid explicit cache directories; they never + * fall back to the default. The directory is never derived from the PTE's + * location. * Copied PTEs can share cache keys. The quiescence and failure contract of * clear_cache also applies. Keep the loader alive and its data unchanged until * the call returns. */ [[nodiscard]] runtime::Error clear_cache_for_pte( runtime::DataLoader& loader, - const char* coreai_assets_dir = nullptr); + const char* coreai_cache_dir = nullptr); /** FileDataLoader convenience overload with the same contract. */ [[nodiscard]] runtime::Error clear_cache_for_pte( const char* pte_path, - const char* coreai_assets_dir = nullptr); + const char* coreai_cache_dir = nullptr); } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_cache.mm b/backends/apple/coreai/runtime/coreai_cache.mm index 369a5164a88..7f20cd861c1 100644 --- a/backends/apple/coreai/runtime/coreai_cache.mm +++ b/backends/apple/coreai/runtime/coreai_cache.mm @@ -7,7 +7,7 @@ */ #include "coreai_cache.h" -#import "coreai_bookmarks.h" +#import "coreai_cache_entry.h" #import "coreai_cache_key.h" #import "coreai_cache_root.h" #import "coreai_load_coordinator.h" @@ -15,32 +15,28 @@ #include "coreai_pte.h" #include +#include namespace executorch::backends::coreai { namespace { using runtime::Error; using runtime::Result; -Result cache_root(const char* path) { - NSString* root = nil; - if (path == nullptr) { - auto default_root = default_coreai_assets_root(); - if (!default_root.ok()) { - return default_root.error(); - } - root = default_root.get(); - } else { - root = [NSString stringWithUTF8String:path]; +Result> cache_directory(const char* path) { + auto configured = cache_directory_path(path); + if (!configured.ok()) { + return configured.error(); } - return resolve_bookmark_root(root, StorageCreationMode::OpenExisting); + return resolve_cache_directory( + configured.get(), StorageCreationMode::OpenExisting); } -Error clear_keys(NSString* root, NSArray* keys) { +Error clear_keys(const CacheDirectory& directory, NSArray* keys) { NSArray* ordered = [[[NSSet setWithArray:keys] allObjects] sortedArrayUsingSelector:@selector(compare:)]; Error first_error = Error::Ok; for (NSString* key in ordered) { - const Error error = clear_bookmark_assets(root, key, nil); + const Error error = clear_cache_entry(directory, key, nil); if (error != Error::Ok) { ET_LOG( Error, @@ -55,13 +51,15 @@ Error clear_keys(NSString* root, NSArray* keys) { return first_error; } -Error clear_pte(runtime::DataLoader& loader, NSString* root) { +Error clear_pte( + runtime::DataLoader& loader, + const std::optional& directory) { // Keep the verified image and selected segment owners alive through eviction. auto pte = inspect_coreai_pte(loader); if (!pte.ok()) { return pte.error(); } - NSMutableSet* model_prefixes = [NSMutableSet set]; + NSMutableSet* bundle_identities = [NSMutableSet set]; for (const auto& buffer : pte->processed) { auto data = buffer.data_safe(); if (!data.ok()) { @@ -76,57 +74,60 @@ Error clear_pte(runtime::DataLoader& loader, NSString* root) { return manifest.error(); } for (NSString* bundle_name in manifest->bundle_digests) { - auto prefix = cache_model_key(manifest.get(), bundle_name); - if (!prefix.ok()) { - return prefix.error(); + auto identity = bundle_identity(manifest.get(), bundle_name); + if (!identity.ok()) { + return identity.error(); } - [model_prefixes addObject:prefix.get()]; + [bundle_identities addObject:identity.get()]; } } - if (model_prefixes.count == 0) { + if (bundle_identities.count == 0 || !directory.has_value()) { return Error::Ok; } - auto inventory = inventory_cache_keys(root); + auto inventory = inventory_cache_keys(*directory); if (!inventory.ok()) { return inventory.error(); } NSMutableArray* keys = [NSMutableArray array]; for (NSString* key in inventory.get()) { - if ([model_prefixes containsObject:cache_key_model_prefix(key)]) { + if ([bundle_identities containsObject:cache_key_bundle_identity(key)]) { [keys addObject:key]; } } - return clear_keys(root, keys); + return clear_keys(*directory, keys); } } // namespace -Error clear_cache(const char* coreai_assets_dir) { +Error clear_cache(const char* coreai_cache_dir) { @autoreleasepool { - auto root = cache_root(coreai_assets_dir); - if (!root.ok()) { - return root.error(); + auto directory = cache_directory(coreai_cache_dir); + if (!directory.ok()) { + return directory.error(); + } + if (!directory->has_value()) { + return Error::Ok; } - auto keys = inventory_cache_keys(root.get()); + auto keys = inventory_cache_keys(directory->value()); if (!keys.ok()) { return keys.error(); } - return clear_keys(root.get(), keys.get()); + return clear_keys(directory->value(), keys.get()); } } Error clear_cache_for_pte( runtime::DataLoader& loader, - const char* coreai_assets_dir) { + const char* coreai_cache_dir) { @autoreleasepool { - auto root = cache_root(coreai_assets_dir); - if (!root.ok()) { - return root.error(); + auto directory = cache_directory(coreai_cache_dir); + if (!directory.ok()) { + return directory.error(); } - return clear_pte(loader, root.get()); + return clear_pte(loader, directory.get()); } } -Error clear_cache_for_pte(const char* pte_path, const char* coreai_assets_dir) { +Error clear_cache_for_pte(const char* pte_path, const char* coreai_cache_dir) { ET_CHECK_OR_RETURN_ERROR( pte_path != nullptr && pte_path[0] != '\0', InvalidArgument, @@ -135,7 +136,7 @@ Error clear_cache_for_pte(const char* pte_path, const char* coreai_assets_dir) { if (!loader.ok()) { return loader.error(); } - return clear_cache_for_pte(loader.get(), coreai_assets_dir); + return clear_cache_for_pte(loader.get(), coreai_cache_dir); } } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_cache_entry.h b/backends/apple/coreai/runtime/coreai_cache_entry.h new file mode 100644 index 00000000000..7ff03361247 --- /dev/null +++ b/backends/apple/coreai/runtime/coreai_cache_entry.h @@ -0,0 +1,116 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ +#pragma once + +#import +#include +#include +#include +#include "coreai_cache_root.h" + +namespace executorch::backends::coreai { + +// Invalid and unowned names are omitted from the sorted inventory. +runtime::Result*> inventory_cache_keys( + const CacheDirectory& directory); + +enum class CacheBookmarkState { + Missing, + Corrupt, + Present, +}; + +struct CacheBookmarkInspection final { + CacheBookmarkState state; + NSData* data; +}; + +enum class CacheEntryPersistence { + BookmarkCapable, + StagingOnly, +}; + +struct CacheStagingDirectory final { + FileDescriptor descriptor; + NSURL* url; +}; + +class CacheEntryMaintenance; + +class LockedCacheEntry final { + public: + LockedCacheEntry(const LockedCacheEntry&) = delete; + LockedCacheEntry& operator=(const LockedCacheEntry&) = delete; + + // Each acquisition owns an independent descriptor on one stable stripe. + static runtime::Result> acquire( + const CacheDirectory& directory, + NSString* key); + // Missing entries return a null handle without changing entry storage. + static runtime::Result> acquire_existing( + const CacheDirectory& directory, + NSString* key); + + CacheEntryPersistence persistence() const { + return persistence_; + } + NSString* key() const { + return key_; + } + // Missing is nil. Empty, oversized, inaccessible, or nonregular files fail + // closed. + runtime::Result read_bookmark() const; + runtime::Error write_bookmark(NSData* data) const; + runtime::Result prepare_staging_directory() const; + // Missing staging is a no-op; unexpected entry types fail before deletion. + runtime::Error remove_staging() const; + + // The view must not outlive this locked entry. + CacheEntryMaintenance maintenance() const; + + private: + friend class CacheEntryMaintenance; + + LockedCacheEntry( + FileDescriptor root, + FileDescriptor bookmarks, + FileDescriptor lock_file, + NSString* root_path, + NSString* key, + CacheEntryPersistence persistence); + static runtime::Result> acquire_impl( + const CacheDirectory& directory, + NSString* key, + StorageCreationMode mode); + runtime::Result> open_staging_directory( + StorageCreationMode mode) const; + + FileDescriptor root_; + FileDescriptor bookmarks_; + FileDescriptor lock_file_; + NSString* root_path_; + NSString* key_; + CacheEntryPersistence persistence_; +}; + +class CacheEntryMaintenance final { + public: + runtime::Result inspect_bookmark() const; + runtime::Error preflight() const; + runtime::Error remove_staging() const; + runtime::Error remove_bookmark() const; + + private: + friend class LockedCacheEntry; + explicit CacheEntryMaintenance(const LockedCacheEntry& entry) + : entry_(entry) {} + + const LockedCacheEntry& entry_; +}; + +} // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_bookmarks.mm b/backends/apple/coreai/runtime/coreai_cache_entry.mm similarity index 57% rename from backends/apple/coreai/runtime/coreai_bookmarks.mm rename to backends/apple/coreai/runtime/coreai_cache_entry.mm index cad63fa6e35..d8fa58900e0 100644 --- a/backends/apple/coreai/runtime/coreai_bookmarks.mm +++ b/backends/apple/coreai/runtime/coreai_cache_entry.mm @@ -5,10 +5,9 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -#import "coreai_bookmarks.h" +#import "coreai_cache_entry.h" #import "coreai_cache_key.h" -#import "coreai_cache_root.h" #import "coreai_fault_injection.h" #import "coreai_storage.h" #import "coreai_validation.h" @@ -18,6 +17,7 @@ #include #include #include +#include namespace executorch::backends::coreai { namespace { @@ -34,32 +34,16 @@ return [key stringByAppendingString:kBookmarkExtension]; } -// Returns -1 when the root does not exist. -Result open_root(NSString* root) { - const int fd = retry_eintr([&] { - return open( - root.fileSystemRepresentation, - O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); - }); - if (fd < 0 && errno == ENOENT) { - return -1; - } - ET_CHECK_OR_RETURN_ERROR( - fd >= 0, AccessFailed, "Cannot open Core AI assets root"); - return fd; -} - -// Returns -1 when the child does not exist in OpenExisting mode. -Result open_child(int root_fd, NSString* name, StorageCreationMode mode) { +Result> +open_child(int root_fd, NSString* name, StorageCreationMode mode) { auto opened = open_storage_directory(root_fd, name, mode); if (!opened.ok()) { return opened.error(); } - FileDescriptor child(opened.get().fd); - if (opened.get().created) { + if (opened->created) { ET_CHECK_OK_OR_RETURN_ERROR(sync_storage_directory(root_fd)); } - return child.release(); + return std::move(opened->descriptor); } Result entry_exists(int root_fd, NSString* key) { @@ -70,14 +54,15 @@ if (!opened.ok()) { return opened.error(); } - FileDescriptor directory(opened.get()); - if (directory.get() < 0) { + if (!opened->has_value()) { continue; } NSString* entry = [directory_name isEqual:kBookmarksDirectory] ? bookmark_name(key) : key; struct stat info; - if (lstat_at(directory.get(), entry.fileSystemRepresentation, &info) == 0) { + if (lstat_at( + opened->value().get(), entry.fileSystemRepresentation, &info) == + 0) { return true; } ET_CHECK_OR_RETURN_ERROR( @@ -85,39 +70,102 @@ } return false; } + +Result duplicate_directory(int fd) { + FileDescriptor duplicate( + retry_eintr([&] { return fcntl(fd, F_DUPFD_CLOEXEC, 0); })); + ET_CHECK_OR_RETURN_ERROR( + duplicate.get() >= 0, + AccessFailed, + "Cannot retain Core AI cache directory"); + return duplicate; +} + } // namespace -Result> acquire_cache_entry_lock( - NSString* root, +Result*> inventory_cache_keys( + const CacheDirectory& directory) { + NSMutableSet* keys = [NSMutableSet set]; + for (NSString* child_name in @[ kBookmarksDirectory, kStagingDirectory ]) { + auto child = open_child( + directory.descriptor.get(), + child_name, + StorageCreationMode::OpenExisting); + if (!child.ok()) { + return child.error(); + } + if (!child->has_value()) { + continue; + } + auto names = + storage_children(child->value().get(), StorageNamePolicy::SkipInvalid); + if (!names.ok()) { + return names.error(); + } + for (NSString* name in names.get()) { + NSString* key = name; + if ([child_name isEqual:kBookmarksDirectory]) { + if (![name hasSuffix:kBookmarkExtension] || + name.length <= kBookmarkExtension.length) { + continue; + } + key = [name substringToIndex:name.length - kBookmarkExtension.length]; + } + if (is_cache_key(key)) { + [keys addObject:key]; + } + } + } + return [[keys allObjects] sortedArrayUsingSelector:@selector(compare:)]; +} + +LockedCacheEntry::LockedCacheEntry( + FileDescriptor root, + FileDescriptor bookmarks, + FileDescriptor lock_file, + NSString* root_path, + NSString* key, + CacheEntryPersistence persistence) + : root_(std::move(root)), + bookmarks_(std::move(bookmarks)), + lock_file_(std::move(lock_file)), + root_path_(root_path), + key_(key), + persistence_(persistence) {} + +Result> LockedCacheEntry::acquire( + const CacheDirectory& directory, + NSString* key) { + return acquire_impl(directory, key, StorageCreationMode::Create); +} + +Result> LockedCacheEntry::acquire_existing( + const CacheDirectory& directory, + NSString* key) { + return acquire_impl(directory, key, StorageCreationMode::OpenExisting); +} + +Result> LockedCacheEntry::acquire_impl( + const CacheDirectory& cache_directory, NSString* key, StorageCreationMode mode) { auto lock_name = cache_entry_lock_name(key); if (!lock_name.ok()) { return lock_name.error(); } - auto resolved = resolve_bookmark_root(root, mode); - if (!resolved.ok()) { - return resolved.error(); + auto duplicated = duplicate_directory(cache_directory.descriptor.get()); + if (!duplicated.ok()) { + return duplicated.error(); } - root = [resolved.get() copy]; - auto opened = open_root(root); - if (!opened.ok()) { - return opened.error(); - } - FileDescriptor directory(opened.get()); + FileDescriptor directory = std::move(duplicated.get()); const bool create = mode == StorageCreationMode::Create; - if (directory.get() < 0) { - ET_CHECK_OR_RETURN_ERROR( - !create, AccessFailed, "Core AI assets root disappeared"); - return std::unique_ptr(); - } if (!create) { auto exists = entry_exists(directory.get(), key); if (!exists.ok()) { return exists.error(); } if (!exists.get()) { - return std::unique_ptr(); + return std::unique_ptr(); } } auto locks = @@ -125,9 +173,8 @@ if (!locks.ok()) { return locks.error(); } - FileDescriptor lock_directory(locks.get()); FileDescriptor lock_file(open_storage_shared_file( - lock_directory.get(), lock_name.get().fileSystemRepresentation)); + locks->value().get(), lock_name.get().fileSystemRepresentation)); struct stat info; ET_CHECK_OR_RETURN_ERROR( lock_file.get() >= 0 && fstat(lock_file.get(), &info) == 0 && @@ -148,111 +195,28 @@ FileDescriptor lock_file(open_storage_shared_file( return exists.error(); } if (!exists.get()) { - return std::unique_ptr(); + return std::unique_ptr(); } } auto bookmarks = open_child(directory.get(), kBookmarksDirectory, mode); if (!bookmarks.ok()) { return bookmarks.error(); } - FileDescriptor bookmark_directory(bookmarks.get()); - const CacheEntryPersistence persistence = bookmark_directory.get() >= 0 + const CacheEntryPersistence persistence = bookmarks->has_value() ? CacheEntryPersistence::BookmarkCapable : CacheEntryPersistence::StagingOnly; - return std::unique_ptr(new CacheEntryLock( - directory.release(), - bookmark_directory.release(), - lock_file.release(), - root, + FileDescriptor bookmark_directory = + bookmarks->has_value() ? std::move(bookmarks->value()) : FileDescriptor(); + return std::unique_ptr(new LockedCacheEntry( + std::move(directory), + std::move(bookmark_directory), + std::move(lock_file), + [cache_directory.path copy], [key copy], persistence)); } -Result resolve_bookmark_root( - NSString* path, - StorageCreationMode mode) { - ET_CHECK_OR_RETURN_ERROR( - nonempty_string(path) && [path hasPrefix:@"/"], - InvalidArgument, - "Core AI assets require an absolute root path"); - if (mode == StorageCreationMode::Create) { - return prepare_storage_root(path, mode); - } - struct stat info; - const int result = - retry_eintr([&] { return stat(path.fileSystemRepresentation, &info); }); - if (result < 0 && errno == ENOENT) { - return path; - } - ET_CHECK_OR_RETURN_ERROR( - result == 0 && S_ISDIR(info.st_mode), - AccessFailed, - "Invalid Core AI assets root"); - return path; -} - -Result*> inventory_cache_keys(NSString* root) { - auto resolved = - resolve_bookmark_root(root, StorageCreationMode::OpenExisting); - if (!resolved.ok()) { - return resolved.error(); - } - auto opened = open_root(resolved.get()); - if (!opened.ok()) { - return opened.error(); - } - FileDescriptor directory(opened.get()); - if (directory.get() < 0) { - return @[]; - } - NSMutableSet* keys = [NSMutableSet set]; - for (NSString* child_name in @[ kBookmarksDirectory, kStagingDirectory ]) { - auto child = open_child( - directory.get(), child_name, StorageCreationMode::OpenExisting); - if (!child.ok()) { - return child.error(); - } - FileDescriptor child_directory(child.get()); - if (child_directory.get() < 0) { - continue; - } - auto names = - storage_children(child_directory.get(), StorageNamePolicy::SkipInvalid); - if (!names.ok()) { - return names.error(); - } - for (NSString* name in names.get()) { - NSString* key = name; - if ([child_name isEqual:kBookmarksDirectory]) { - if (![name hasSuffix:kBookmarkExtension] || - name.length <= kBookmarkExtension.length) { - continue; - } - key = [name substringToIndex:name.length - kBookmarkExtension.length]; - } - if (is_cache_key(key)) { - [keys addObject:key]; - } - } - } - return [[keys allObjects] sortedArrayUsingSelector:@selector(compare:)]; -} - -CacheEntryLock::CacheEntryLock( - int root, - int bookmarks, - int lock_file, - NSString* root_path, - NSString* key, - CacheEntryPersistence persistence) - : root_(root), - bookmarks_(bookmarks), - lock_file_(lock_file), - root_path_(root_path), - key_(key), - persistence_(persistence) {} - -Result CacheEntryLock::read_bookmark() const { +Result LockedCacheEntry::read_bookmark() const { if (persistence_ == CacheEntryPersistence::StagingOnly) { return static_cast(nil); } @@ -293,7 +257,7 @@ FileDescriptor file(retry_eintr([&] { return bytes; } -Error CacheEntryLock::write_bookmark(NSData* data) const { +Error LockedCacheEntry::write_bookmark(NSData* data) const { ET_CHECK_OR_RETURN_ERROR( [data isKindOfClass:NSData.class] && data.length > 0 && data.length <= kBookmarkByteLimit, @@ -306,38 +270,49 @@ FileDescriptor file(retry_eintr([&] { return publish_storage_data(bookmarks_.get(), bookmark_name(key_), data); } -Result CacheEntryLock::prepare_staging_root() const { - auto staging = - open_child(root_.get(), kStagingDirectory, StorageCreationMode::Create); +Result> LockedCacheEntry::open_staging_directory( + StorageCreationMode mode) const { + return open_child(root_.get(), kStagingDirectory, mode); +} + +Result LockedCacheEntry::prepare_staging_directory() + const { + auto staging = open_staging_directory(StorageCreationMode::Create); if (!staging.ok()) { return staging.error(); } - FileDescriptor directory(staging.get()); - return [root_path_ stringByAppendingPathComponent:kStagingDirectory]; + NSURL* url = [[NSURL fileURLWithPath:root_path_ isDirectory:YES] + URLByAppendingPathComponent:kStagingDirectory + isDirectory:YES]; + return CacheStagingDirectory{std::move(staging->value()), url}; } -Error CacheEntryLock::remove_staging() const { - auto opened = open_child( - root_.get(), kStagingDirectory, StorageCreationMode::OpenExisting); - if (!opened.ok()) { - return opened.error(); +Error LockedCacheEntry::remove_staging() const { + auto staging = open_staging_directory(StorageCreationMode::OpenExisting); + if (!staging.ok()) { + return staging.error(); } - FileDescriptor staging(opened.get()); - if (staging.get() < 0) { + if (!staging->has_value()) { return Error::Ok; } return remove_storage_directory( - staging.get(), key_, StorageRemovalPolicy::Strict); + staging->value().get(), key_, StorageRemovalPolicy::Strict); } -Result -CacheEntryLock::inspect_bookmark_for_maintenance() const { - if (persistence_ == CacheEntryPersistence::StagingOnly) { +CacheEntryMaintenance LockedCacheEntry::maintenance() const { + return CacheEntryMaintenance(*this); +} + +Result CacheEntryMaintenance::inspect_bookmark() + const { + if (entry_.persistence_ == CacheEntryPersistence::StagingOnly) { return CacheBookmarkInspection{CacheBookmarkState::Missing, nil}; } struct stat info; const int inspected = lstat_at( - bookmarks_.get(), bookmark_name(key_).fileSystemRepresentation, &info); + entry_.bookmarks_.get(), + bookmark_name(entry_.key_).fileSystemRepresentation, + &info); if (inspected < 0 && errno == ENOENT) { return CacheBookmarkInspection{CacheBookmarkState::Missing, nil}; } @@ -346,7 +321,7 @@ FileDescriptor file(retry_eintr([&] { if (!S_ISREG(info.st_mode)) { return CacheBookmarkInspection{CacheBookmarkState::Corrupt, nil}; } - auto bookmark = read_bookmark(); + auto bookmark = entry_.read_bookmark(); if (!bookmark.ok()) { if (bookmark.error() == Error::InvalidExternalData) { return CacheBookmarkInspection{CacheBookmarkState::Corrupt, nil}; @@ -359,58 +334,46 @@ FileDescriptor file(retry_eintr([&] { bookmark.get()}; } -Error CacheEntryLock::preflight_maintenance() const { - if (persistence_ == CacheEntryPersistence::BookmarkCapable) { +Error CacheEntryMaintenance::preflight() const { + if (entry_.persistence_ == CacheEntryPersistence::BookmarkCapable) { ET_CHECK_OK_OR_RETURN_ERROR(preflight_storage_directory( - bookmarks_.get(), - bookmark_name(key_), + entry_.bookmarks_.get(), + bookmark_name(entry_.key_), StorageRemovalPolicy::Maintenance)); } - auto opened = open_child( - root_.get(), kStagingDirectory, StorageCreationMode::OpenExisting); - if (!opened.ok()) { - return opened.error(); + auto staging = + entry_.open_staging_directory(StorageCreationMode::OpenExisting); + if (!staging.ok()) { + return staging.error(); } - FileDescriptor staging(opened.get()); - if (staging.get() < 0) { + if (!staging->has_value()) { return Error::Ok; } return preflight_storage_directory( - staging.get(), key_, StorageRemovalPolicy::Maintenance); + staging->value().get(), entry_.key_, StorageRemovalPolicy::Maintenance); } -Error CacheEntryLock::remove_staging_for_maintenance() const { - auto opened = open_child( - root_.get(), kStagingDirectory, StorageCreationMode::OpenExisting); - if (!opened.ok()) { - return opened.error(); +Error CacheEntryMaintenance::remove_staging() const { + auto staging = + entry_.open_staging_directory(StorageCreationMode::OpenExisting); + if (!staging.ok()) { + return staging.error(); } - FileDescriptor staging(opened.get()); - if (staging.get() < 0) { + if (!staging->has_value()) { return Error::Ok; } return remove_storage_directory( - staging.get(), key_, StorageRemovalPolicy::Maintenance); + staging->value().get(), entry_.key_, StorageRemovalPolicy::Maintenance); } -Error CacheEntryLock::remove_bookmark_for_maintenance() const { - if (persistence_ == CacheEntryPersistence::StagingOnly) { +Error CacheEntryMaintenance::remove_bookmark() const { + if (entry_.persistence_ == CacheEntryPersistence::StagingOnly) { return Error::Ok; } return remove_storage_directory( - bookmarks_.get(), bookmark_name(key_), StorageRemovalPolicy::Maintenance); -} - -Result> lock_cache_entry( - NSString* root, - NSString* key) { - return acquire_cache_entry_lock(root, key, StorageCreationMode::Create); -} - -Result> lock_existing_cache_entry( - NSString* root, - NSString* key) { - return acquire_cache_entry_lock(root, key, StorageCreationMode::OpenExisting); + entry_.bookmarks_.get(), + bookmark_name(entry_.key_), + StorageRemovalPolicy::Maintenance); } } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_cache_key.h b/backends/apple/coreai/runtime/coreai_cache_key.h index 4ea30a6e1c0..3f91fa04640 100644 --- a/backends/apple/coreai/runtime/coreai_cache_key.h +++ b/backends/apple/coreai/runtime/coreai_cache_key.h @@ -17,14 +17,14 @@ class Manifest; class SelectedBundle; bool is_cache_key(NSString* key); -NSString* cache_key_model_prefix(NSString* key); +NSString* cache_key_bundle_identity(NSString* key); -// Model identity excludes device and specialization so maintenance can find all -// variants of one exported bundle. -runtime::Result cache_model_key( +// Bundle identity excludes device and specialization so maintenance can find +// every runtime variant of one exported bundle. +runtime::Result bundle_identity( const Manifest& manifest, NSString* bundle_name); -runtime::Result cache_model_key(const SelectedBundle& bundle); +runtime::Result bundle_identity(const SelectedBundle& bundle); // specialization_identity is stable, versioned plain data supplied by the SDK // boundary. This module does not depend on SDK types. runtime::Result cache_variant_key( diff --git a/backends/apple/coreai/runtime/coreai_cache_key.mm b/backends/apple/coreai/runtime/coreai_cache_key.mm index 87b0f9a9f2b..7dafbf83c69 100644 --- a/backends/apple/coreai/runtime/coreai_cache_key.mm +++ b/backends/apple/coreai/runtime/coreai_cache_key.mm @@ -74,10 +74,12 @@ bool valid_device(NSString* device) { return [result copy]; } -Result -model_key(NSString* digest, NSString* bundle_name, bool aot_compiled) { +Result compute_bundle_identity( + NSString* digest, + NSString* bundle_name, + bool aot_compiled) { ET_CHECK_OR_RETURN_ERROR( - lowercase_hex(digest, kHashLength) && relative_path(bundle_name) && + digest != nil && is_relative_path(bundle_name) && ![bundle_name containsString:@"/"], DelegateInvalidCompatibility, "Invalid Core AI bundle identity"); @@ -91,7 +93,7 @@ bool valid_device(NSString* device) { Result utf8_data(NSString* value) { ET_CHECK_OR_RETURN_ERROR( - nonempty_string(value), + is_nonempty_string(value), DelegateInvalidCompatibility, "Missing Core AI cache specialization"); NSData* data = [value dataUsingEncoding:NSUTF8StringEncoding]; @@ -113,8 +115,8 @@ bool is_cache_key(NSString* key) { } NSString* model = [key substringToIndex:kHashLength]; NSString* options = [key substringFromIndex:options_start]; - if (!lowercase_hex(model, kHashLength) || - !lowercase_hex(options, kHashLength)) { + if (!is_lowercase_hex(model, kHashLength) || + !is_lowercase_hex(options, kHashLength)) { return false; } return valid_device( @@ -122,19 +124,22 @@ bool is_cache_key(NSString* key) { kHashLength + 1, key.length - kFixedLength)]); } -NSString* cache_key_model_prefix(NSString* key) { +NSString* cache_key_bundle_identity(NSString* key) { return is_cache_key(key) ? [key substringToIndex:kHashLength] : nil; } -Result cache_model_key( +Result bundle_identity( const Manifest& manifest, NSString* bundle_name) { - return model_key( - manifest.bundle_digests[bundle_name], bundle_name, manifest.aot_compiled); + NSString* digest = manifest.bundle_digests[bundle_name]; + ET_CHECK_OR_RETURN_ERROR( + digest != nil, InvalidArgument, "Unknown Core AI bundle name"); + return compute_bundle_identity(digest, bundle_name, manifest.aot_compiled); } -Result cache_model_key(const SelectedBundle& bundle) { - return model_key(bundle.digest, bundle.bundle_name, bundle.aot_compiled); +Result bundle_identity(const SelectedBundle& bundle) { + return compute_bundle_identity( + bundle.digest, bundle.bundle_name, bundle.aot_compiled); } Result cache_variant_key( @@ -142,12 +147,12 @@ bool is_cache_key(NSString* key) { NSString* platform, NSString* architecture, NSString* specialization_identity) { - auto model = cache_model_key(bundle); + auto model = bundle_identity(bundle); if (!model.ok()) { return model.error(); } ET_CHECK_OR_RETURN_ERROR( - nonempty_string(platform) && nonempty_string(architecture) && + is_nonempty_string(platform) && is_nonempty_string(architecture) && ![platform containsString:@"_"], DelegateInvalidCompatibility, "Invalid Core AI cache context"); diff --git a/backends/apple/coreai/runtime/coreai_cache_root.h b/backends/apple/coreai/runtime/coreai_cache_root.h index ee536e241fc..143c6b86132 100644 --- a/backends/apple/coreai/runtime/coreai_cache_root.h +++ b/backends/apple/coreai/runtime/coreai_cache_root.h @@ -10,15 +10,23 @@ #import #include -#include "coreai_file.h" +#include +#include "coreai_storage.h" namespace executorch::backends::coreai { +struct CacheDirectory final { + NSString* path; + FileDescriptor descriptor; +}; + // Resolves the default location without creating or modifying storage. -runtime::Result default_coreai_assets_root(); -// Requires an absolute directory path. With Create, makes the directory and -// excludes it from backup; otherwise a missing directory is InvalidArgument. -runtime::Result prepare_storage_root( +runtime::Result default_cache_directory(); +// Null selects the default; invalid explicit paths never fall back to it. +runtime::Result cache_directory_path(const char* configured_path); +// Resolves symlinks once and anchors the resulting directory with an owned +// descriptor. OpenExisting returns no value when the directory is missing. +runtime::Result> resolve_cache_directory( NSString* path, StorageCreationMode mode); diff --git a/backends/apple/coreai/runtime/coreai_cache_root.mm b/backends/apple/coreai/runtime/coreai_cache_root.mm index 18ca1b6f198..f4b7d4123f1 100644 --- a/backends/apple/coreai/runtime/coreai_cache_root.mm +++ b/backends/apple/coreai/runtime/coreai_cache_root.mm @@ -8,24 +8,29 @@ #import "coreai_cache_root.h" #include "coreai_fault_injection.h" -#include "coreai_file.h" +#include "coreai_validation.h" +#include #include +#include #include +#include +#include +#include namespace executorch::backends::coreai { namespace { using runtime::Error; using runtime::Result; -Error ensure_excluded_from_backup(NSURL* directory) { - NSURL* fresh = [NSURL fileURLWithPath:directory.path isDirectory:YES]; +Error ensure_excluded_from_backup(NSString* path) { + NSURL* directory = [NSURL fileURLWithPath:path isDirectory:YES]; NSNumber* excluded = nil; NSError* error = nil; ET_CHECK_OR_RETURN_ERROR( - [fresh getResourceValue:&excluded - forKey:NSURLIsExcludedFromBackupKey - error:&error], + [directory getResourceValue:&excluded + forKey:NSURLIsExcludedFromBackupKey + error:&error], AccessFailed, "Cannot read Core AI backup exclusion: %s", error.localizedDescription.UTF8String); @@ -33,18 +38,33 @@ Error ensure_excluded_from_backup(NSURL* directory) { return Error::Ok; } ET_CHECK_OR_RETURN_ERROR( - [fresh setResourceValue:@YES - forKey:NSURLIsExcludedFromBackupKey - error:&error], + [directory setResourceValue:@YES + forKey:NSURLIsExcludedFromBackupKey + error:&error], AccessFailed, - "Cannot exclude Core AI storage from backup: %s", + "Cannot exclude Core AI cache directory from backup: %s", error.localizedDescription.UTF8String); return Error::Ok; } -} // namespace +Result canonical_path(NSString* path) { + char* resolved = realpath(path.fileSystemRepresentation, nullptr); + ET_CHECK_OR_RETURN_ERROR( + resolved != nullptr, + AccessFailed, + "Cannot resolve Core AI cache directory"); + NSString* canonical = [NSFileManager.defaultManager + stringWithFileSystemRepresentation:resolved + length:std::strlen(resolved)]; + std::free(resolved); + ET_CHECK_OR_RETURN_ERROR( + canonical != nil, + AccessFailed, + "Cannot decode Core AI cache directory path"); + return canonical; +} -Result default_coreai_assets_root() { +Result default_cache_directory_impl() { NSURL* caches = [NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] @@ -54,27 +74,59 @@ Error ensure_excluded_from_backup(NSURL* directory) { return [caches.path stringByAppendingPathComponent:@"executorch_coreai"]; } -Result prepare_storage_root( +} // namespace + +Result default_cache_directory() { + return default_cache_directory_impl(); +} + +Result cache_directory_path(const char* configured_path) { + if (configured_path == nullptr) { + return default_cache_directory(); + } + NSString* path = [NSString stringWithUTF8String:configured_path]; + ET_CHECK_OR_RETURN_ERROR( + is_nonempty_string(path) && path.isAbsolutePath && ![path hasPrefix:@"~"], + InvalidArgument, + "Core AI cache directory requires an absolute UTF-8 path"); + return path; +} + +Result> resolve_cache_directory( NSString* path, StorageCreationMode mode) { const bool create = mode == StorageCreationMode::Create; ET_CHECK_OR_RETURN_ERROR( - [path isKindOfClass:NSString.class] && [path hasPrefix:@"/"] && - [path rangeOfString:@"\0"].location == NSNotFound, + is_nonempty_string(path) && path.isAbsolutePath && ![path hasPrefix:@"~"], InvalidArgument, - "Core AI storage requires an absolute path"); + "Core AI cache directory requires an absolute path"); + struct stat link_info; + const int link_inspected = retry_eintr( + [&] { return lstat(path.fileSystemRepresentation, &link_info); }); struct stat info; - const int result = + int inspected = retry_eintr([&] { return stat(path.fileSystemRepresentation, &info); }); + if (inspected < 0 && errno == ENOENT && !create) { + return std::optional(); + } + if (inspected < 0 && errno == ENOTDIR) { + if (create) { + return Error::InvalidArgument; + } + return Error::AccessFailed; + } ET_CHECK_OR_RETURN_ERROR( - result == 0 || errno == ENOENT || errno == ENOTDIR, + inspected == 0 || (create && errno == ENOENT), AccessFailed, - "Cannot inspect Core AI storage directory"); - ET_CHECK_OR_RETURN_ERROR( - result == 0 ? S_ISDIR(info.st_mode) : create && errno == ENOENT, - InvalidArgument, - "Core AI storage must be a directory"); - if (create && result != 0) { + "Cannot inspect Core AI cache directory"); + if (inspected == 0 && !S_ISDIR(info.st_mode)) { + ET_LOG(Error, "Core AI cache path is not a directory"); + if (create) { + return Error::InvalidArgument; + } + return Error::AccessFailed; + } + if (inspected < 0) { NSDictionary* attributes = @{ NSFileProtectionKey : NSFileProtectionCompleteUntilFirstUserAuthentication, @@ -82,7 +134,7 @@ Error ensure_excluded_from_backup(NSURL* directory) { ET_CHECK_OR_RETURN_ERROR( fault_point(FaultPoint::BeforeCreateDirectory) == 0, AccessFailed, - "Cannot begin Core AI directory creation"); + "Cannot begin Core AI cache directory creation"); NSError* error = nil; ET_CHECK_OR_RETURN_ERROR( [NSFileManager.defaultManager createDirectoryAtPath:path @@ -90,14 +142,36 @@ Error ensure_excluded_from_backup(NSURL* directory) { attributes:attributes error:&error], AccessFailed, - "Cannot create Core AI storage directory: %s", + "Cannot create Core AI cache directory: %s", error.localizedDescription.UTF8String); } + NSString* resolved_path = path; + if (link_inspected == 0 && S_ISLNK(link_info.st_mode)) { + auto canonical = canonical_path(path); + if (!canonical.ok()) { + return canonical.error(); + } + resolved_path = canonical.get(); + } + FileDescriptor directory(retry_eintr([&] { + return open( + resolved_path.fileSystemRepresentation, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + })); + if (directory.get() < 0 && errno == ENOENT && !create) { + return std::optional(); + } + ET_CHECK_OR_RETURN_ERROR( + directory.get() >= 0 && fstat(directory.get(), &info) == 0 && + S_ISDIR(info.st_mode), + AccessFailed, + "Cannot open Core AI cache directory"); if (create) { - ET_CHECK_OK_OR_RETURN_ERROR(ensure_excluded_from_backup( - [NSURL fileURLWithPath:path isDirectory:YES])); + ET_CHECK_OK_OR_RETURN_ERROR(ensure_excluded_from_backup(resolved_path)); } - return path; + return std::optional( + std::in_place, + CacheDirectory{[resolved_path copy], std::move(directory)}); } } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_fault_injection.h b/backends/apple/coreai/runtime/coreai_fault_injection.h index 4e72901c775..275ef14e157 100644 --- a/backends/apple/coreai/runtime/coreai_fault_injection.h +++ b/backends/apple/coreai/runtime/coreai_fault_injection.h @@ -8,6 +8,10 @@ #pragma once +#include +#include +#include "coreai_file.h" + namespace executorch::backends::coreai { enum class FaultPoint { @@ -39,4 +43,16 @@ constexpr int fault_point(FaultPoint) { } #endif +template +auto fault_aware_retry(FaultPoint point, F&& call) { + return retry_eintr([&]() -> decltype(call()) { + const int fault = fault_point(point); + if (fault != 0) { + errno = fault; + return -1; + } + return call(); + }); +} + } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_file.h b/backends/apple/coreai/runtime/coreai_file.h index cbc991a5fae..19433fbf2e5 100644 --- a/backends/apple/coreai/runtime/coreai_file.h +++ b/backends/apple/coreai/runtime/coreai_file.h @@ -22,12 +22,19 @@ enum class StorageCreationMode { class FileDescriptor { public: - explicit FileDescriptor(int fd) : fd_(fd) {} + explicit FileDescriptor(int fd = -1) : fd_(fd) {} ~FileDescriptor() { reset(-1); } FileDescriptor(const FileDescriptor&) = delete; FileDescriptor& operator=(const FileDescriptor&) = delete; + FileDescriptor(FileDescriptor&& other) noexcept : fd_(other.release()) {} + FileDescriptor& operator=(FileDescriptor&& other) noexcept { + if (this != &other) { + reset(other.release()); + } + return *this; + } int get() const { return fd_; } diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.h b/backends/apple/coreai/runtime/coreai_load_coordinator.h index 03d59f3cde2..5bb7655674f 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.h +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.h @@ -9,29 +9,31 @@ #include #import "ETCoreAIBridge.h" +#import "coreai_cache_root.h" #import "coreai_manifest.h" namespace executorch::backends::coreai { -struct RuntimeAssets final { +struct SelectedHostBundle final { SelectedBundle bundle; NSString* const platform; NSString* const architecture; }; -runtime::Result select_runtime_assets(const Manifest& manifest); +runtime::Result select_host_bundle( + const Manifest& manifest); // The cache entry lock outlives all SDK callbacks and bookmark publication. // Cleanup after a restore hit or durable publication is best-effort. -runtime::Result> acquire_bookmark_model( - const RuntimeAssets& assets, +runtime::Result> acquire_model( + const SelectedHostBundle& assets, const runtime::NamedDataMap* named_data, - NSString* root, + const CacheDirectory& cache_directory, id loader); // Callers must keep affected loads and sessions quiescent across processes. -runtime::Error clear_bookmark_assets( - NSString* root, +runtime::Error clear_cache_entry( + const CacheDirectory& cache_directory, NSString* key, id loader); diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.mm b/backends/apple/coreai/runtime/coreai_load_coordinator.mm index ab0d7149fb4..6e89c14cf60 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.mm +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.mm @@ -7,7 +7,7 @@ */ #import "coreai_load_coordinator.h" -#import "coreai_bookmarks.h" +#import "coreai_cache_entry.h" #import "coreai_cache_key.h" #import "coreai_error.h" #import "coreai_fault_injection.h" @@ -70,7 +70,7 @@ }); } -void cleanup_source(const CacheEntryLock& cache_entry) { +void cleanup_source(const LockedCacheEntry& cache_entry) { const Error error = cache_entry.remove_staging(); if (error != Error::Ok) { ET_LOG( @@ -114,7 +114,7 @@ Error evict_one(id loader, NSData* bookmark) { } } // namespace -Result select_runtime_assets(const Manifest& manifest) { +Result select_host_bundle(const Manifest& manifest) { ET_CHECK_OR_RETURN_ERROR( [NSProcessInfo.processInfo isOperatingSystemAtLeastVersion:manifest.minimum_version], @@ -132,18 +132,18 @@ Error evict_one(id loader, NSData* bookmark) { NSString* platform = @"unsupported"; #endif NSString* architecture = ETCoreAIDeviceArchitectureName(); - auto selected = select_assets(manifest, architecture, platform); + auto selected = select_bundle(manifest, architecture, platform); if (!selected.ok()) { return selected.error(); } - return RuntimeAssets{ + return SelectedHostBundle{ std::move(selected.get()), [platform copy], [architecture copy]}; } -Result> acquire_bookmark_model( - const RuntimeAssets& assets, +Result> acquire_model( + const SelectedHostBundle& assets, const runtime::NamedDataMap* named_data, - NSString* root, + const CacheDirectory& cache_directory, id loader) { ET_CHECK_OR_RETURN_ERROR( loader != nil, InvalidArgument, "Missing Core AI model loader"); @@ -155,7 +155,7 @@ Error evict_one(id loader, NSData* bookmark) { } NSString* cache_key = key.get(); ET_LOG(Info, "Core AI cache lookup: key=%s", cache_key.UTF8String); - auto locked = lock_cache_entry(root, cache_key); + auto locked = LockedCacheEntry::acquire(cache_directory, cache_key); if (!locked.ok()) { return locked.error(); } @@ -216,11 +216,11 @@ Error evict_one(id loader, NSData* bookmark) { return model; } -Error clear_bookmark_assets( - NSString* root, +Error clear_cache_entry( + const CacheDirectory& cache_directory, NSString* key, id loader) { - auto locked = lock_existing_cache_entry(root, key); + auto locked = LockedCacheEntry::acquire_existing(cache_directory, key); if (!locked.ok()) { return locked.error(); } @@ -228,11 +228,12 @@ Error clear_bookmark_assets( return Error::Ok; } ET_LOG(Info, "Core AI cache clear started: key=%s", key.UTF8String); - auto bookmark = locked.get()->inspect_bookmark_for_maintenance(); + auto maintenance = locked.get()->maintenance(); + auto bookmark = maintenance.inspect_bookmark(); if (!bookmark.ok()) { return bookmark.error(); } - ET_CHECK_OK_OR_RETURN_ERROR(locked.get()->preflight_maintenance()); + ET_CHECK_OK_OR_RETURN_ERROR(maintenance.preflight()); if (bookmark->state != CacheBookmarkState::Present) { ET_LOG( Info, @@ -257,8 +258,8 @@ Error clear_bookmark_assets( "Core AI SDK cache entry absence confirmed: key=%s", key.UTF8String); } - ET_CHECK_OK_OR_RETURN_ERROR(locked.get()->remove_staging_for_maintenance()); - ET_CHECK_OK_OR_RETURN_ERROR(locked.get()->remove_bookmark_for_maintenance()); + ET_CHECK_OK_OR_RETURN_ERROR(maintenance.remove_staging()); + ET_CHECK_OK_OR_RETURN_ERROR(maintenance.remove_bookmark()); ET_LOG(Info, "Core AI local cache entry cleared: key=%s", key.UTF8String); return Error::Ok; } diff --git a/backends/apple/coreai/runtime/coreai_manifest.h b/backends/apple/coreai/runtime/coreai_manifest.h index c929e9cf632..9627761c0ce 100644 --- a/backends/apple/coreai/runtime/coreai_manifest.h +++ b/backends/apple/coreai/runtime/coreai_manifest.h @@ -15,7 +15,7 @@ namespace executorch::backends::coreai { class Manifest final { public: - NSString* const model_hash; + NSString* const export_hash; NSString* const function; NSArray* const inputs; NSArray* const outputs; @@ -34,7 +34,7 @@ class Manifest final { private: Manifest( - NSString* model_hash, + NSString* export_hash, NSString* function, NSArray* inputs, NSArray* outputs, @@ -50,7 +50,7 @@ class Manifest final { class SelectedBundle final { public: - NSString* const model_hash; + NSString* const export_hash; NSString* const path; NSString* const bundle_name; NSString* const digest; @@ -71,7 +71,7 @@ class SelectedBundle final { NSString* path, NSDictionary* files); - friend runtime::Result select_assets( + friend runtime::Result select_bundle( const Manifest& manifest, NSString* device_architecture, NSString* platform); @@ -79,7 +79,7 @@ class SelectedBundle final { runtime::Result parse_manifest(NSData* data); runtime::Result parse_manifest(const void* data, size_t size); -runtime::Result select_assets( +runtime::Result select_bundle( const Manifest& manifest, NSString* device_architecture, NSString* platform); diff --git a/backends/apple/coreai/runtime/coreai_manifest.mm b/backends/apple/coreai/runtime/coreai_manifest.mm index 06ffff5e832..8d2a056d2df 100644 --- a/backends/apple/coreai/runtime/coreai_manifest.mm +++ b/backends/apple/coreai/runtime/coreai_manifest.mm @@ -14,7 +14,7 @@ namespace executorch::backends::coreai { Manifest::Manifest( - NSString* model_hash, + NSString* export_hash, NSString* function, NSArray* inputs, NSArray* outputs, @@ -24,7 +24,7 @@ NSString* platform, NSDictionary* architectures, NSOperatingSystemVersion minimum_version) - : model_hash(model_hash), + : export_hash(export_hash), function(function), inputs(inputs), outputs(outputs), @@ -39,7 +39,7 @@ const Manifest& manifest, NSString* path, NSDictionary* files) - : model_hash(manifest.model_hash), + : export_hash(manifest.export_hash), path(path), bundle_name(path.lastPathComponent), digest(manifest.bundle_digests[bundle_name]), @@ -59,7 +59,7 @@ bool names_array(id value) { } NSMutableSet* seen = [NSMutableSet set]; for (id name in value) { - if (!nonempty_string(name) || [seen containsObject:name]) { + if (!is_nonempty_string(name) || [seen containsObject:name]) { return false; } [seen addObject:name]; @@ -91,7 +91,7 @@ Error validate_files( NSLocale* locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; for (NSString* file in filenames) { ET_CHECK_OR_RETURN_ERROR( - relative_path(file) && file_size(filenames[file]) && + is_relative_path(file) && file_size(filenames[file]) && [bundles containsObject:[file componentsSeparatedByString:@"/"] .firstObject] && [file containsString:@"/"] && @@ -132,7 +132,7 @@ bool parse_version(id value, NSOperatingSystemVersion& version) { version = {}; return true; } - if (!nonempty_string(value)) { + if (!is_nonempty_string(value)) { return false; } NSArray* parts = [value componentsSeparatedByString:@"."]; @@ -179,21 +179,23 @@ bool parse_version(id value, NSOperatingSystemVersion& version) { NotSupported, "Unsupported Core AI packaging; expected .aimodel or AOT .aimodelc"); const bool aot_compiled = [packaging hasPrefix:@"aot_compiled_"]; - NSString* model_hash = dict[@"hash"]; + NSString* export_hash = dict[@"export_hash"]; NSString* function = dict[@"function"]; NSArray* inputs = dict[@"input_names"]; NSArray* outputs = dict[@"output_names"]; NSString* platform = nil; NSDictionary* architectures = nil; ET_CHECK_OR_RETURN_ERROR( - lowercase_hex(model_hash), InvalidProgram, "Invalid Core AI model hash"); + is_lowercase_hex(export_hash), + InvalidProgram, + "Invalid Core AI export hash"); NSMutableSet* bundles = [NSMutableSet set]; NSString* source_path = nil; if (aot_compiled) { platform = dict[@"platform"]; architectures = dict[@"archs"]; ET_CHECK_OR_RETURN_ERROR( - nonempty_string(platform), + is_nonempty_string(platform), InvalidProgram, "Core AI AOT manifest requires a target platform"); ET_CHECK_OR_RETURN_ERROR( @@ -203,15 +205,15 @@ bool parse_version(id value, NSOperatingSystemVersion& version) { "Core AI AOT manifest requires a nonempty architecture map"); for (id arch in architectures) { ET_CHECK_OR_RETURN_ERROR( - relative_path(arch) && ![arch containsString:@"/"] && + is_relative_path(arch) && ![arch containsString:@"/"] && [arch canBeConvertedToEncoding:NSUTF8StringEncoding], InvalidProgram, "Invalid Core AI AOT architecture name"); NSString* bundle = [NSString stringWithFormat:@"model.%@.aimodelc", arch]; id path = architectures[arch]; ET_CHECK_OR_RETURN_ERROR( - relative_path(path) && - [path isEqualToString:[model_hash + is_relative_path(path) && + [path isEqualToString:[export_hash stringByAppendingPathComponent:bundle]], InvalidProgram, "Invalid Core AI AOT bundle path for architecture %s", @@ -221,16 +223,17 @@ bool parse_version(id value, NSOperatingSystemVersion& version) { } else { source_path = dict[@"path"]; ET_CHECK_OR_RETURN_ERROR( - relative_path(source_path) && + is_relative_path(source_path) && [source_path - isEqualToString:[model_hash + isEqualToString:[export_hash stringByAppendingString:@"/model.aimodel"]], InvalidProgram, "Invalid Core AI source bundle path"); [bundles addObject:@"model.aimodel"]; } ET_CHECK_OR_RETURN_ERROR( - nonempty_string(function) && names_array(inputs) && names_array(outputs), + is_nonempty_string(function) && names_array(inputs) && + names_array(outputs), InvalidProgram, "Core AI manifest requires ordered input/output names"); NSOperatingSystemVersion minimum_version{}; @@ -249,12 +252,12 @@ bool parse_version(id value, NSOperatingSystemVersion& version) { for (NSString* bundle in bundles) { id digest = bundle_digests[bundle]; ET_CHECK_OR_RETURN_ERROR( - lowercase_hex(digest, 64), + is_lowercase_hex(digest, 64), InvalidProgram, "Invalid Core AI exported bundle digest"); } return Manifest( - model_hash, + export_hash, function, inputs, outputs, @@ -272,14 +275,14 @@ bool parse_version(id value, NSOperatingSystemVersion& version) { return parse_manifest([NSData dataWithBytes:data length:size]); } -Result select_assets( +Result select_bundle( const Manifest& manifest, NSString* device_architecture, NSString* platform) { if (!manifest.aot_compiled) { return SelectedBundle( manifest, - [manifest.model_hash stringByAppendingString:@"/model.aimodel"], + [manifest.export_hash stringByAppendingString:@"/model.aimodel"], manifest.files); } ET_CHECK_OR_RETURN_ERROR( @@ -295,7 +298,7 @@ bool parse_version(id value, NSOperatingSystemVersion& version) { }]; NSString* available = [architectures componentsJoinedByString:@", "]; ET_CHECK_OR_RETURN_ERROR( - nonempty_string(device_architecture), + is_nonempty_string(device_architecture), DelegateInvalidCompatibility, "Core AI SDK device architecture is unavailable (%s); available " "architectures: [%s]; re-export the model as .aimodel or for this device", diff --git a/backends/apple/coreai/runtime/coreai_source_bundle.h b/backends/apple/coreai/runtime/coreai_source_bundle.h index dabaeef5c62..6dae0d68447 100644 --- a/backends/apple/coreai/runtime/coreai_source_bundle.h +++ b/backends/apple/coreai/runtime/coreai_source_bundle.h @@ -14,7 +14,7 @@ namespace executorch::backends::coreai { -class CacheEntryLock; +class LockedCacheEntry; class SelectedBundle; enum class CorruptBundlePolicy { @@ -27,7 +27,7 @@ enum class CorruptBundlePolicy { runtime::Result prepare_source_bundle( const SelectedBundle& bundle, const runtime::NamedDataMap* named_data, - const CacheEntryLock& cache_entry, + const LockedCacheEntry& cache_entry, CorruptBundlePolicy corrupt_policy = CorruptBundlePolicy::Reject); } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_source_bundle.mm b/backends/apple/coreai/runtime/coreai_source_bundle.mm index 916637e2e68..36cda105d5c 100644 --- a/backends/apple/coreai/runtime/coreai_source_bundle.mm +++ b/backends/apple/coreai/runtime/coreai_source_bundle.mm @@ -8,7 +8,7 @@ #import "coreai_source_bundle.h" -#import "coreai_bookmarks.h" +#import "coreai_cache_entry.h" #import "coreai_fault_injection.h" #import "coreai_manifest.h" #import "coreai_storage.h" @@ -18,6 +18,7 @@ #include #include #include +#include namespace executorch::backends::coreai { namespace { @@ -144,14 +145,8 @@ FileDescriptor entry( Error fully_sync_directory(int fd) { ET_CHECK_OK_OR_RETURN_ERROR(sync_storage_directory(fd)); ET_CHECK_OR_RETURN_ERROR( - retry_eintr([&] { - const int fault = fault_point(FaultPoint::FullSync); - if (fault != 0) { - errno = fault; - return -1; - } - return fcntl(fd, F_FULLFSYNC); - }) == 0, + fault_aware_retry( + FaultPoint::FullSync, [&] { return fcntl(fd, F_FULLFSYNC); }) == 0, AccessFailed, "Cannot fully synchronize Core AI source directory"); return Error::Ok; @@ -189,26 +184,18 @@ Error fully_sync_directory(int fd) { NSString* name = [@".staging-" stringByAppendingString:NSUUID.UUID.UUIDString]; ET_CHECK_OR_RETURN_ERROR( - retry_eintr([&] { - const int fault = fault_point(FaultPoint::CreateStagingDirectory); - if (fault != 0) { - errno = fault; - return -1; - } - return mkdirat(root_fd, name.fileSystemRepresentation, 0777); - }) == 0, + fault_aware_retry( + FaultPoint::CreateStagingDirectory, + [&] { + return mkdirat(root_fd, name.fileSystemRepresentation, 0777); + }) == 0, AccessFailed, "Cannot create Core AI staging directory"); return StagingDirectory{root_fd, [name copy]}; } Result open_staging_directory(int root_fd, NSString* name) { - const int fd = retry_eintr([&] { - const int fault = fault_point(FaultPoint::OpenStagingDirectory); - if (fault != 0) { - errno = fault; - return -1; - } + const int fd = fault_aware_retry(FaultPoint::OpenStagingDirectory, [&] { return open_directory_at(root_fd, name.fileSystemRepresentation); }); ET_CHECK_OR_RETURN_ERROR( @@ -223,7 +210,7 @@ Error write_staging_bundle( for (NSString* file in bundle.files) { @autoreleasepool { NSString* data_key = - [NSString stringWithFormat:@"coreai/%@/%@", bundle.model_hash, file]; + [NSString stringWithFormat:@"coreai/%@/%@", bundle.export_hash, file]; auto buffer = named_data.get_data(data_key.UTF8String); if (!buffer.ok()) { ET_CHECK_OR_RETURN_ERROR( @@ -250,12 +237,7 @@ Error write_staging_bundle( int root_fd, StagingDirectory& staging, NSString* key) { - const int result = retry_eintr([&] { - const int fault = fault_point(FaultPoint::Rename); - if (fault != 0) { - errno = fault; - return -1; - } + const int result = fault_aware_retry(FaultPoint::Rename, [&] { return renameatx_np( root_fd, staging.name.fileSystemRepresentation, @@ -293,24 +275,18 @@ Error recover_lost_publication_race( Result prepare_source_bundle( const SelectedBundle& bundle, const runtime::NamedDataMap* named_data, - const CacheEntryLock& cache_entry, + const LockedCacheEntry& cache_entry, CorruptBundlePolicy corrupt_policy) { NSString* key = cache_entry.key(); - auto prepared = cache_entry.prepare_staging_root(); + auto prepared = cache_entry.prepare_staging_directory(); if (!prepared.ok()) { return prepared.error(); } - NSURL* root = [NSURL fileURLWithPath:prepared.get() isDirectory:YES]; - NSURL* model_url = [[root URLByAppendingPathComponent:key isDirectory:YES] + NSURL* model_url = [[prepared->url URLByAppendingPathComponent:key + isDirectory:YES] URLByAppendingPathComponent:bundle.bundle_name isDirectory:YES]; - FileDescriptor root_fd(retry_eintr([&] { - return open( - root.fileSystemRepresentation, - O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); - })); - ET_CHECK_OR_RETURN_ERROR( - root_fd.get() >= 0, AccessFailed, "Cannot open Core AI source root"); + FileDescriptor root_fd = std::move(prepared->descriptor); auto state = inspect_source_bundle(root_fd.get(), key, bundle.files); if (!state.ok()) { diff --git a/backends/apple/coreai/runtime/coreai_storage.h b/backends/apple/coreai/runtime/coreai_storage.h index a7fed0578ed..16180b30800 100644 --- a/backends/apple/coreai/runtime/coreai_storage.h +++ b/backends/apple/coreai/runtime/coreai_storage.h @@ -11,18 +11,18 @@ #import #include #include +#include #include "coreai_file.h" namespace executorch::backends::coreai { runtime::Error sync_storage_directory(int fd); -struct StorageDirectory { - int fd; +struct StorageDirectory final { + std::optional descriptor; bool created; }; -// Opens one child component without following symlinks. The caller owns fd; -// a missing child with OpenExisting returns {-1, false}. Does not flush -// directories. +// Opens one child component without following symlinks. A missing child with +// OpenExisting has no descriptor. Does not flush directories. runtime::Result open_storage_directory(int parent_fd, NSString* name, StorageCreationMode mode); // Returns an independently owned descriptor for a stable coordination file. diff --git a/backends/apple/coreai/runtime/coreai_storage.mm b/backends/apple/coreai/runtime/coreai_storage.mm index 553eff015b1..79e28c8bde7 100644 --- a/backends/apple/coreai/runtime/coreai_storage.mm +++ b/backends/apple/coreai/runtime/coreai_storage.mm @@ -19,24 +19,13 @@ #include #include #include +#include namespace executorch::backends::coreai { namespace { using runtime::Error; using runtime::Result; -template -auto storage_call(FaultPoint point, F call) { - return retry_eintr([&]() -> decltype(call()) { - const int fault = fault_point(point); - if (fault != 0) { - errno = fault; - return -1; - } - return call(); - }); -} - enum class StorageSyncMode { Normal, Full, @@ -56,7 +45,9 @@ Error write_file( "Invalid Core AI storage write"); for (NSString* component in components) { ET_CHECK_OR_RETURN_ERROR( - storage_component(component), InvalidArgument, "Invalid storage path"); + is_storage_component(component), + InvalidArgument, + "Invalid storage path"); } FileDescriptor parent( retry_eintr([&] { return fcntl(root_fd, F_DUPFD_CLOEXEC, 0); })); @@ -67,9 +58,9 @@ FileDescriptor parent( parent.get(), components[i], StorageCreationMode::Create); if (!opened.ok()) return opened.error(); - FileDescriptor next(opened.get().fd); + FileDescriptor next = std::move(*opened->descriptor); ET_CHECK_OK_OR_RETURN_ERROR(sync_storage_directory(parent.get())); - parent.reset(next.release()); + parent = std::move(next); } FileDescriptor file(retry_eintr([&] { return openat( @@ -83,7 +74,7 @@ FileDescriptor file(retry_eintr([&] { const auto result = [&]() -> Error { const auto* bytes = static_cast(data); while (size > 0) { - const ssize_t count = storage_call(FaultPoint::Write, [&] { + const ssize_t count = fault_aware_retry(FaultPoint::Write, [&] { return write(file.get(), bytes, std::min(size, size_t(1024 * 1024))); }); ET_CHECK_OR_RETURN_ERROR( @@ -92,10 +83,10 @@ FileDescriptor file(retry_eintr([&] { size -= count; } ET_CHECK_OR_RETURN_ERROR( - storage_call(FaultPoint::SyncFile, [&] { return fsync(file.get()); }) == - 0 && + fault_aware_retry( + FaultPoint::SyncFile, [&] { return fsync(file.get()); }) == 0 && (sync_mode != StorageSyncMode::Full || - storage_call( + fault_aware_retry( FaultPoint::FullSync, [&] { return fcntl(file.get(), F_FULLFSYNC); }) == 0), AccessFailed, @@ -125,6 +116,62 @@ FileDescriptor file(retry_eintr([&] { Remove, }; +enum class StorageEntryState { + Missing, + NonDirectory, + Directory, +}; + +struct InspectedStorageEntry final { + StorageEntryState state; + std::optional directory; + struct stat info; +}; + +Result inspect_storage_entry( + int parent_fd, + NSString* name, + StorageRemovalPolicy policy) { + ET_CHECK_OR_RETURN_ERROR( + is_storage_component(name), + InvalidArgument, + "Invalid Core AI entry name"); + struct stat info {}; + const int inspected = + lstat_at(parent_fd, name.fileSystemRepresentation, &info); + if (inspected < 0 && errno == ENOENT) { + return InspectedStorageEntry{ + StorageEntryState::Missing, std::nullopt, info}; + } + ET_CHECK_OR_RETURN_ERROR( + inspected == 0, AccessFailed, "Cannot inspect Core AI storage entry"); + if (!S_ISDIR(info.st_mode)) { + ET_CHECK_OR_RETURN_ERROR( + policy == StorageRemovalPolicy::Maintenance, + InvalidExternalData, + "Core AI storage entry is not a directory"); + return InspectedStorageEntry{ + StorageEntryState::NonDirectory, std::nullopt, info}; + } + FileDescriptor entry( + open_directory_at(parent_fd, name.fileSystemRepresentation)); + if (entry.get() < 0 && errno == ENOENT) { + return InspectedStorageEntry{ + StorageEntryState::Missing, std::nullopt, info}; + } + ET_CHECK_OR_RETURN_ERROR( + entry.get() >= 0, AccessFailed, "Cannot open Core AI storage directory"); + return InspectedStorageEntry{ + StorageEntryState::Directory, + std::optional(std::move(entry)), + info}; +} + +bool same_storage_entry(const struct stat& left, const struct stat& right) { + return left.st_dev == right.st_dev && left.st_ino == right.st_ino && + (left.st_mode & S_IFMT) == (right.st_mode & S_IFMT); +} + Error walk_storage_tree( int fd, StorageTreePass pass, @@ -161,7 +208,17 @@ FileDescriptor child( ET_CHECK_OK_OR_RETURN_ERROR(walk_storage_tree(child.get(), pass, policy)); } if (pass == StorageTreePass::Remove) { - const int removed = storage_call(FaultPoint::Remove, [&] { + struct stat current; + const int revalidated = + lstat_at(fd, name.fileSystemRepresentation, ¤t); + if (revalidated < 0 && errno == ENOENT) { + continue; + } + ET_CHECK_OR_RETURN_ERROR( + revalidated == 0 && same_storage_entry(info, current), + AccessFailed, + "Core AI storage entry changed during removal"); + const int removed = fault_aware_retry(FaultPoint::Remove, [&] { return unlinkat( fd, name.fileSystemRepresentation, directory ? AT_REMOVEDIR : 0); }); @@ -182,7 +239,7 @@ FileDescriptor child( NSString* name, StorageCreationMode mode) { ET_CHECK_OR_RETURN_ERROR( - storage_component(name), + is_storage_component(name), InvalidArgument, "Invalid storage directory name"); const auto open_directory = [&] { @@ -207,10 +264,10 @@ FileDescriptor child( fd = open_directory(); } if (fd < 0 && errno == ENOENT && !create) - return StorageDirectory{-1, false}; + return StorageDirectory{std::nullopt, false}; ET_CHECK_OR_RETURN_ERROR( fd >= 0, AccessFailed, "Cannot open Core AI storage directory"); - return StorageDirectory{fd, created}; + return StorageDirectory{FileDescriptor(fd), created}; } Result*> storage_children(int fd, StorageNamePolicy policy) { @@ -263,53 +320,39 @@ Error preflight_storage_directory( int parent_fd, NSString* name, StorageRemovalPolicy policy) { - ET_CHECK_OR_RETURN_ERROR( - storage_component(name), InvalidArgument, "Invalid Core AI entry name"); - struct stat info; - const int inspected = - lstat_at(parent_fd, name.fileSystemRepresentation, &info); - if (inspected < 0 && errno == ENOENT) { - return Error::Ok; - } - ET_CHECK_OR_RETURN_ERROR( - inspected == 0, AccessFailed, "Cannot inspect Core AI storage entry"); - if (!S_ISDIR(info.st_mode)) { - ET_CHECK_OR_RETURN_ERROR( - policy == StorageRemovalPolicy::Maintenance, - InvalidExternalData, - "Core AI storage entry is not a directory"); - return Error::Ok; - } - FileDescriptor entry( - open_directory_at(parent_fd, name.fileSystemRepresentation)); - if (entry.get() < 0 && errno == ENOENT) { - return Error::Ok; + auto entry = inspect_storage_entry(parent_fd, name, policy); + if (!entry.ok()) { + return entry.error(); } - ET_CHECK_OR_RETURN_ERROR( - entry.get() >= 0, AccessFailed, "Cannot open Core AI storage directory"); - return walk_storage_tree(entry.get(), StorageTreePass::Preflight, policy); + return entry->state == StorageEntryState::Directory + ? walk_storage_tree( + entry->directory->get(), StorageTreePass::Preflight, policy) + : Error::Ok; } Error remove_storage_directory( int parent_fd, NSString* name, StorageRemovalPolicy policy) { - ET_CHECK_OR_RETURN_ERROR( - storage_component(name), InvalidArgument, "Invalid Core AI entry name"); - struct stat info; - const int inspected = - lstat_at(parent_fd, name.fileSystemRepresentation, &info); - if (inspected < 0 && errno == ENOENT) { + auto entry = inspect_storage_entry(parent_fd, name, policy); + if (!entry.ok()) { + return entry.error(); + } + if (entry->state == StorageEntryState::Missing) { return Error::Ok; } - ET_CHECK_OR_RETURN_ERROR( - inspected == 0, AccessFailed, "Cannot inspect Core AI storage entry"); - if (!S_ISDIR(info.st_mode)) { + if (entry->state == StorageEntryState::NonDirectory) { + struct stat current; + const int revalidated = + lstat_at(parent_fd, name.fileSystemRepresentation, ¤t); + if (revalidated < 0 && errno == ENOENT) { + return Error::Ok; + } ET_CHECK_OR_RETURN_ERROR( - policy == StorageRemovalPolicy::Maintenance, - InvalidExternalData, - "Core AI storage entry is not a directory"); - const int removed = storage_call(FaultPoint::Remove, [&] { + revalidated == 0 && same_storage_entry(entry->info, current), + AccessFailed, + "Core AI storage entry changed during removal"); + const int removed = fault_aware_retry(FaultPoint::Remove, [&] { return unlinkat(parent_fd, name.fileSystemRepresentation, 0); }); ET_CHECK_OR_RETURN_ERROR( @@ -318,18 +361,21 @@ Error remove_storage_directory( "Cannot remove Core AI storage entry"); return sync_storage_directory(parent_fd); } - FileDescriptor entry( - open_directory_at(parent_fd, name.fileSystemRepresentation)); - if (entry.get() < 0 && errno == ENOENT) { + ET_CHECK_OK_OR_RETURN_ERROR(walk_storage_tree( + entry->directory->get(), StorageTreePass::Preflight, policy)); + ET_CHECK_OK_OR_RETURN_ERROR(walk_storage_tree( + entry->directory->get(), StorageTreePass::Remove, policy)); + struct stat current; + const int revalidated = + lstat_at(parent_fd, name.fileSystemRepresentation, ¤t); + if (revalidated < 0 && errno == ENOENT) { return sync_storage_directory(parent_fd); } ET_CHECK_OR_RETURN_ERROR( - entry.get() >= 0, AccessFailed, "Cannot open Core AI storage directory"); - ET_CHECK_OK_OR_RETURN_ERROR( - walk_storage_tree(entry.get(), StorageTreePass::Preflight, policy)); - ET_CHECK_OK_OR_RETURN_ERROR( - walk_storage_tree(entry.get(), StorageTreePass::Remove, policy)); - const int removed = storage_call(FaultPoint::Remove, [&] { + revalidated == 0 && same_storage_entry(entry->info, current), + AccessFailed, + "Core AI storage directory changed during removal"); + const int removed = fault_aware_retry(FaultPoint::Remove, [&] { return unlinkat(parent_fd, name.fileSystemRepresentation, AT_REMOVEDIR); }); ET_CHECK_OR_RETURN_ERROR( @@ -358,7 +404,8 @@ int open_storage_shared_file(int parent_fd, const char* name) { Error sync_storage_directory(int fd) { ET_CHECK_OR_RETURN_ERROR( - storage_call(FaultPoint::SyncDirectory, [&] { return fsync(fd); }) == 0, + fault_aware_retry(FaultPoint::SyncDirectory, [&] { return fsync(fd); }) == + 0, AccessFailed, "Cannot synchronize Core AI directory"); return Error::Ok; @@ -375,7 +422,7 @@ Error write_storage_file( Error publish_storage_data(int root_fd, NSString* name, NSData* data) { ET_CHECK_OR_RETURN_ERROR( - storage_component(name) && data != nil, + is_storage_component(name) && data != nil, InvalidArgument, "Invalid Core AI storage publication"); NSString* temporary = @@ -383,7 +430,7 @@ Error publish_storage_data(int root_fd, NSString* name, NSData* data) { // Bookmarks are small and costly to lose, so they get a full device flush. auto result = write_file( root_fd, temporary, data.bytes, data.length, StorageSyncMode::Full); - if (result == Error::Ok && storage_call(FaultPoint::Rename, [&] { + if (result == Error::Ok && fault_aware_retry(FaultPoint::Rename, [&] { return renameat( root_fd, temporary.fileSystemRepresentation, @@ -396,7 +443,7 @@ Error publish_storage_data(int root_fd, NSString* name, NSData* data) { if (result == Error::Ok) { // Flush the renamed entry as well as the file's pre-rename contents. ET_CHECK_OR_RETURN_ERROR( - storage_call( + fault_aware_retry( FaultPoint::FullSync, [&] { return fcntl(root_fd, F_FULLFSYNC); }) == 0, AccessFailed, diff --git a/backends/apple/coreai/runtime/coreai_validation.h b/backends/apple/coreai/runtime/coreai_validation.h index ea21885b19d..14f5e9fd63e 100644 --- a/backends/apple/coreai/runtime/coreai_validation.h +++ b/backends/apple/coreai/runtime/coreai_validation.h @@ -13,15 +13,15 @@ namespace executorch::backends::coreai { // Nonempty text without embedded NULs. -bool nonempty_string(id value); +bool is_nonempty_string(id value); // Relative bundle path without escapes, backslashes, or empty components. -bool relative_path(NSString* path); +bool is_relative_path(NSString* path); // Single path component for an fd-relative open: no slashes, ".", or "..". -// Unlike relative_path, backslashes are ordinary bytes on this filesystem. -bool storage_component(NSString* name); -// Lowercase hex of any positive length, as in the exporter model hash. -bool lowercase_hex(NSString* value); +// Unlike is_relative_path, backslashes are ordinary bytes on this filesystem. +bool is_storage_component(NSString* name); +// Lowercase hex of any positive length, as in the export hash. +bool is_lowercase_hex(NSString* value); // Lowercase hex of exactly length characters, as in bundle digests. -bool lowercase_hex(NSString* value, NSUInteger length); +bool is_lowercase_hex(NSString* value, NSUInteger length); } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_validation.mm b/backends/apple/coreai/runtime/coreai_validation.mm index 54c611b81bc..4b635772561 100644 --- a/backends/apple/coreai/runtime/coreai_validation.mm +++ b/backends/apple/coreai/runtime/coreai_validation.mm @@ -19,13 +19,13 @@ } // namespace -bool nonempty_string(id value) { +bool is_nonempty_string(id value) { return [value isKindOfClass:NSString.class] && [value length] > 0 && [value rangeOfString:@"\0"].location == NSNotFound; } -bool relative_path(NSString* path) { - if (!nonempty_string(path) || path.isAbsolutePath || +bool is_relative_path(NSString* path) { + if (!is_nonempty_string(path) || path.isAbsolutePath || [path containsString:@"\\"]) { return false; } @@ -38,19 +38,19 @@ bool relative_path(NSString* path) { return true; } -bool storage_component(NSString* name) { - return nonempty_string(name) && ![name isEqualToString:@"."] && +bool is_storage_component(NSString* name) { + return is_nonempty_string(name) && ![name isEqualToString:@"."] && ![name isEqualToString:@".."] && ![name containsString:@"/"]; } -bool lowercase_hex(NSString* value) { - return nonempty_string(value) && +bool is_lowercase_hex(NSString* value) { + return is_nonempty_string(value) && [value rangeOfCharacterFromSet:invalid_hex_characters()].location == NSNotFound; } -bool lowercase_hex(NSString* value, NSUInteger length) { - return lowercase_hex(value) && [value length] == length; +bool is_lowercase_hex(NSString* value, NSUInteger length) { + return is_lowercase_hex(value) && [value length] == length; } } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.h b/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.h index edd5eea7f53..75fe312d026 100644 --- a/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.h +++ b/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.h @@ -11,6 +11,7 @@ #include "coreai_bookmark_fixture.h" #include "coreai_fake_loader.h" #include "coreai_fault_injection.h" +#include "coreai_load_coordinator.h" #include "coreai_source_fixture.h" namespace executorch::backends::coreai::testing { @@ -18,6 +19,15 @@ namespace executorch::backends::coreai::testing { runtime::Result acquisition_staged_url( NSURL* root, const SelectedBundle& bundle); +runtime::Result> acquire_test_model( + const SelectedHostBundle& bundle, + const runtime::NamedDataMap* data, + NSString* cache_path, + id loader); +runtime::Error clear_test_cache_entry( + NSString* cache_path, + NSString* key, + id loader); struct ParallelAcquisitionObservation { int creation[2] = {-1, -1}; diff --git a/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.mm b/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.mm index 935b377bea1..51ef4ae665c 100644 --- a/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.mm +++ b/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.mm @@ -20,16 +20,13 @@ #include #include "coreai_fault_scope.h" #include "coreai_load_coordinator.h" +#include "coreai_test_utils.h" namespace executorch::backends::coreai::testing { namespace { using runtime::Error; using runtime::Result; -dispatch_time_t deadline() { - return dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC); -} - struct AcquisitionWorker { const SelectedBundle* bundle = nullptr; TestData* data = nullptr; @@ -44,8 +41,8 @@ dispatch_time_t deadline() { auto* worker = static_cast(opaque); dispatch_semaphore_signal(worker->started); @autoreleasepool { - RuntimeAssets assets{*worker->bundle, @"macOS", @"arch_b"}; - auto acquired = acquire_bookmark_model( + SelectedHostBundle assets{*worker->bundle, @"macOS", @"arch_b"}; + auto acquired = acquire_test_model( assets, worker->data, worker->root, worker->loader); worker->error = acquired.ok() ? Error::Ok : acquired.error(); worker->model = acquired.ok() && acquired.get() != nil; @@ -80,12 +77,12 @@ void finish() { if (!started[i]) continue; observation.completed[i] = - dispatch_semaphore_wait(workers[i].done, deadline()) == 0; + dispatch_semaphore_wait(workers[i].done, test_deadline()) == 0; observation.joined[i] = pthread_join(threads[i], nullptr); observation.errors[i] = workers[i].error; observation.models[i] = workers[i].model; } - observation.callbacks_drained = bridge.wait_for_callbacks(deadline()); + observation.callbacks_drained = bridge.wait_for_callbacks(test_deadline()); if (observation.callbacks_drained) loader.onSpecialize = nil; finished = true; @@ -95,30 +92,31 @@ void finish() { } }; -bool send_child_byte(int fd, char value) { - const auto end = std::chrono::steady_clock::now() + std::chrono::seconds(10); - while (std::chrono::steady_clock::now() < end) { - const auto remaining = - std::chrono::duration_cast( - end - std::chrono::steady_clock::now()) - .count(); - pollfd entry{fd, POLLOUT, 0}; - const int ready = - poll(&entry, 1, static_cast(remaining > 0 ? remaining : 0)); - if (ready < 0 && errno == EINTR) - continue; - if (ready <= 0 || !(entry.revents & POLLOUT)) - return false; - const ssize_t written = write(fd, &value, 1); - if (written == 1) - return true; - if (written < 0 && (errno == EINTR || errno == EAGAIN)) - continue; - return false; +} // namespace + +Result> acquire_test_model( + const SelectedHostBundle& bundle, + const runtime::NamedDataMap* data, + NSString* cache_path, + id loader) { + auto directory = test_cache_directory(cache_path); + if (!directory.ok()) { + return directory.error(); } - return false; + return acquire_model(bundle, data, directory.get(), loader); +} + +Error clear_test_cache_entry( + NSString* cache_path, + NSString* key, + id loader) { + auto directory = + test_cache_directory(cache_path, StorageCreationMode::OpenExisting); + if (!directory.ok()) { + return directory.error() == Error::NotFound ? Error::Ok : directory.error(); + } + return clear_cache_entry(directory.get(), key, loader); } -} // namespace Result acquisition_staged_url( NSURL* root, @@ -146,7 +144,7 @@ ParallelAcquisitionObservation parallel_acquisitions( dispatch_semaphore_t proceed = group.proceed; loader.onSpecialize = ^(NSURL*) { dispatch_semaphore_signal(entered); - if (dispatch_semaphore_wait(proceed, deadline()) != 0) + if (dispatch_semaphore_wait(proceed, test_deadline()) != 0) ++*timeouts; }; for (int i = 0; i < 2; ++i) { @@ -161,17 +159,18 @@ ParallelAcquisitionObservation parallel_acquisitions( group.started[0] = observation.creation[0] == 0; if (group.started[0]) { observation.first_entered = - dispatch_semaphore_wait(entered, deadline()) == 0; + dispatch_semaphore_wait(entered, test_deadline()) == 0; } if (observation.first_entered) { observation.creation[1] = pthread_create( &group.threads[1], nullptr, AcquisitionWorker::run, &group.workers[1]); group.started[1] = observation.creation[1] == 0; if (group.started[1] && - dispatch_semaphore_wait(group.workers[1].started, deadline()) == 0) { + dispatch_semaphore_wait(group.workers[1].started, test_deadline()) == + 0) { const auto second_deadline = same_key ? dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC) - : deadline(); + : test_deadline(); const bool second_entered = dispatch_semaphore_wait(entered, second_deadline) == 0; observation.second_observed = same_key ? !second_entered : second_entered; @@ -314,7 +313,7 @@ int acquisition_child_mode(int argc, char** argv) { auto parsed = parse_manifest(encode(dictionary)); if (!parsed.ok()) return 5; - auto selected = select_assets(parsed.get(), @"arch_b", @"macOS"); + auto selected = select_bundle(parsed.get(), @"arch_b", @"macOS"); if (!selected.ok()) return 5; char* end = nullptr; @@ -338,7 +337,7 @@ int acquisition_child_mode(int argc, char** argv) { auto second_parsed = parse_manifest(encode(second_dictionary)); if (!second_parsed.ok()) return 5; - auto second = select_assets(second_parsed.get(), @"arch_b", @"macOS"); + auto second = select_bundle(second_parsed.get(), @"arch_b", @"macOS"); if (!second.ok()) return 5; parallel_child( @@ -362,8 +361,8 @@ int acquisition_child_mode(int argc, char** argv) { if (event == boundary) _exit(73); }; - RuntimeAssets assets{selected.get(), @"macOS", @"arch_b"}; - auto result = acquire_bookmark_model(assets, &data, root, loader); + SelectedHostBundle assets{selected.get(), @"macOS", @"arch_b"}; + auto result = acquire_test_model(assets, &data, root, loader); _exit(result.ok() ? 9 : 10); } } diff --git a/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm b/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm index d331194a2f3..5ddb116a117 100644 --- a/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm @@ -43,16 +43,16 @@ - (void)specializeModelAtURL:(NSURL*)url namespace { -TEST(CoreAIRuntimeAssetsTest, ProbesHostAndSelectsImmutableBundle) { +TEST(CoreAISelectedHostBundleTest, ProbesHostAndSelectsImmutableBundle) { ScopedFakeBridgeState bridge; auto parsed = parse_manifest(encode(aot_manifest_dict())); ASSERT_TRUE(parsed.ok()); - auto assets = select_runtime_assets(parsed.get()); + auto assets = select_host_bundle(parsed.get()); ASSERT_TRUE(assets.ok()); EXPECT_TRUE([assets->platform isEqual:@"macOS"]); EXPECT_TRUE([assets->architecture isEqual:@"arch_b"]); EXPECT_TRUE([assets->bundle.bundle_name isEqual:@"model.arch_b.aimodelc"]); - EXPECT_TRUE([assets->bundle.model_hash isEqual:parsed->model_hash]); + EXPECT_TRUE([assets->bundle.export_hash isEqual:parsed->export_hash]); } class CoreAIAcquisitionTest : public ::testing::Test { @@ -61,14 +61,14 @@ - (void)specializeModelAtURL:(NSURL*)url ScopedFakeBridgeState bridge; TestDirectory root; TestData data; - std::optional assets; + std::optional assets; NSString* key = nil; void SetUp() override { ASSERT_NE(root.url, nil); auto selected = bookmark_bundle(); ASSERT_TRUE(selected.ok()); - assets.emplace(RuntimeAssets{selected.get(), @"macOS", @"arch_b"}); + assets.emplace(SelectedHostBundle{selected.get(), @"macOS", @"arch_b"}); auto identity = test_cache_key(assets->bundle); ASSERT_TRUE(identity.ok()); key = identity.get(); @@ -77,7 +77,7 @@ void SetUp() override { Error acquire(BookmarkFakeLoader* loader, const NamedDataMap* named_data) { @autoreleasepool { auto model = - acquire_bookmark_model(*assets, named_data, root.url.path, loader); + acquire_test_model(*assets, named_data, root.url.path, loader); if (!model.ok()) return model.error(); return model.get() != nil ? Error::Ok : Error::Internal; @@ -216,7 +216,7 @@ ino_t lock_inode() { ASSERT_TRUE(asset_tree_snapshot(root.url, before)); Error error = Error::Ok; @autoreleasepool { - auto result = acquire_bookmark_model(*assets, &data, root.url.path, nil); + auto result = acquire_test_model(*assets, &data, root.url.path, nil); ASSERT_FALSE(result.ok()); error = result.error(); } @@ -239,7 +239,7 @@ ino_t lock_inode() { loader.restoreStatus = ETCoreAIAcquisitionStatusHit; Error error = Error::Ok; @autoreleasepool { - auto result = acquire_bookmark_model(*assets, &data, root.url.path, loader); + auto result = acquire_test_model(*assets, &data, root.url.path, loader); ASSERT_FALSE(result.ok()); error = result.error(); } @@ -264,7 +264,7 @@ ino_t lock_inode() { loader.restoreModel = model; Error error = Error::Ok; @autoreleasepool { - auto result = acquire_bookmark_model(*assets, &data, root.url.path, loader); + auto result = acquire_test_model(*assets, &data, root.url.path, loader); ASSERT_FALSE(result.ok()); error = result.error(); } @@ -287,7 +287,7 @@ ino_t lock_inode() { loader.restoreStatus = ETCoreAIAcquisitionStatusError; Error error = Error::Ok; @autoreleasepool { - auto result = acquire_bookmark_model(*assets, &data, root.url.path, loader); + auto result = acquire_test_model(*assets, &data, root.url.path, loader); ASSERT_FALSE(result.ok()); error = result.error(); } @@ -301,7 +301,7 @@ ino_t lock_inode() { ASSERT_NE(loader, nil); Error error = Error::Ok; @autoreleasepool { - auto result = acquire_bookmark_model(*assets, &data, root.url.path, loader); + auto result = acquire_test_model(*assets, &data, root.url.path, loader); ASSERT_FALSE(result.ok()); error = result.error(); } diff --git a/backends/apple/coreai/runtime/test/coreai_bookmark_contention_test.mm b/backends/apple/coreai/runtime/test/coreai_bookmark_contention_test.mm index d45614c2f38..4a38473a1c6 100644 --- a/backends/apple/coreai/runtime/test/coreai_bookmark_contention_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_bookmark_contention_test.mm @@ -24,7 +24,7 @@ struct CacheEntryWaiter { std::future attempting; - std::future>> result; + std::future>> result; }; CacheEntryWaiter start_cache_entry_waiter( @@ -32,7 +32,7 @@ CacheEntryWaiter start_cache_entry_waiter( NSString* key, bool existing_only = false) { std::promise attempting; - std::promise>> result; + std::promise>> result; CacheEntryWaiter waiter{attempting.get_future(), result.get_future()}; // Detached promise-backed work cannot block test teardown if acquisition // stalls after a failed assertion. @@ -51,8 +51,8 @@ CacheEntryWaiter start_cache_entry_waiter( } }; result.set_value( - existing_only ? lock_existing_cache_entry(path, key) - : lock_cache_entry(path, key)); + existing_only ? acquire_existing_test_cache_entry(path, key) + : acquire_test_cache_entry(path, key)); } }).detach(); return waiter; @@ -82,7 +82,7 @@ void SetUp() override { TEST_F( CoreAIBookmarkContentionTest, ThreadContentionSerializesSameKeyButNotDifferentStripes) { - auto holder = lock_cache_entry(root.url.path, key); + auto holder = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(holder.ok()); ASSERT_NE(holder.get(), nullptr); auto waiter = start_cache_entry_waiter(root.url.path, key); @@ -112,7 +112,7 @@ void SetUp() override { TEST_F(CoreAIBookmarkContentionTest, CollidingKeysSerializeAcrossThreads) { auto collision = different_cache_key(*bundle, key, true); ASSERT_TRUE(collision.ok()); - auto holder = lock_cache_entry(root.url.path, key); + auto holder = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(holder.ok()); auto waiter = start_cache_entry_waiter(root.url.path, collision.get()); ASSERT_EQ( @@ -132,7 +132,7 @@ void SetUp() override { MissingCollidingLookupDoesNotWaitOrCreateAnotherLock) { auto collision = different_cache_key(*bundle, key, true); ASSERT_TRUE(collision.ok()); - auto holder = lock_cache_entry(root.url.path, key); + auto holder = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(holder.ok()); auto waiter = start_cache_entry_waiter(root.url.path, collision.get(), true); EXPECT_EQ(waiter.result.wait_for(kWaitTimeout), std::future_status::ready); @@ -149,7 +149,7 @@ void SetUp() override { } TEST_F(CoreAIBookmarkContentionTest, ExistingWaiterRechecksEvictedEntry) { - auto holder = lock_cache_entry(root.url.path, key); + auto holder = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(holder.ok()); NSData* bytes = [@"bookmark" dataUsingEncoding:NSUTF8StringEncoding]; ASSERT_EQ(holder.get()->write_bookmark(bytes), Error::Ok); @@ -171,7 +171,7 @@ void SetUp() override { TEST_F( CoreAIBookmarkContentionTest, WaiterOpensBookmarkDirectoryAfterAcquiringLock) { - auto holder = lock_cache_entry(root.url.path, key); + auto holder = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(holder.ok()); auto waiter = start_cache_entry_waiter(root.url.path, key); ASSERT_EQ( @@ -258,7 +258,8 @@ void SetUp() override { NSMutableDictionary* inodes = [NSMutableDictionary dictionary]; for (NSString* name in representatives) { - auto locked = lock_cache_entry(root.url.path, representatives[name]); + auto locked = + acquire_test_cache_entry(root.url.path, representatives[name]); ASSERT_TRUE(locked.ok()); NSURL* file = [[root.url URLByAppendingPathComponent:@"locks"] URLByAppendingPathComponent:name]; @@ -275,7 +276,8 @@ void SetUp() override { ASSERT_EQ(children.count, kCacheEntryLockStripeCount); for (NSString* name in representatives) { - auto locked = lock_cache_entry(root.url.path, representatives[name]); + auto locked = + acquire_test_cache_entry(root.url.path, representatives[name]); ASSERT_TRUE(locked.ok()); struct stat info {}; NSURL* file = [locks URLByAppendingPathComponent:name]; diff --git a/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.h b/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.h index 90e43b5b617..ea55fa3d711 100644 --- a/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.h +++ b/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.h @@ -12,13 +12,21 @@ #include #include #include -#include "coreai_bookmarks.h" +#include "coreai_cache_entry.h" #include "coreai_cache_key.h" #include "coreai_manifest.h" namespace executorch::backends::coreai::testing { runtime::Result bookmark_bundle(bool aot = false); +runtime::Result test_cache_directory( + NSString* path, + StorageCreationMode mode = StorageCreationMode::Create); +runtime::Result> acquire_test_cache_entry( + NSString* path, + NSString* key); +runtime::Result> +acquire_existing_test_cache_entry(NSString* path, NSString* key); runtime::Result test_cache_key(const SelectedBundle& bundle); runtime::Result test_cache_key( const SelectedBundle& bundle, @@ -32,6 +40,7 @@ NSURL* test_cache_lock_url(NSURL* root, NSString* key); runtime::Result saved_bookmark(NSURL* root, NSString* key); runtime::Result sized_bookmark(size_t size, uint32_t tag = 1); +bool send_child_byte(int fd, char byte); void set_bookmark_test_executable(const char* path); // Returns -1 for normal test invocations, otherwise a child-mode exit code. int bookmark_child_mode(int argc, char** argv); diff --git a/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.mm b/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.mm index d40351abeb8..0d06c71e9c1 100644 --- a/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.mm +++ b/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.mm @@ -44,7 +44,7 @@ int remaining_ms(Clock::time_point deadline) { return remaining.count() <= 0 ? 0 : static_cast(remaining.count()); } -bool send_signal_byte(int fd, char byte) { +bool send_child_byte_impl(int fd, char byte) { const auto deadline = Clock::now() + kChildTimeout; while (const int remaining = remaining_ms(deadline)) { pollfd entry{fd, POLLOUT, 0}; @@ -69,15 +69,48 @@ bool send_signal_byte(int fd, char byte) { } // namespace Result bookmark_bundle(bool aot) { - NSData* encoded = encode(aot ? aot_manifest_dict() : manifest_dict()); - if (encoded == nil) { - return Error::InvalidExternalData; + return selected_bundle(aot ? aot_manifest_dict() : manifest_dict()); +} + +Result test_cache_directory( + NSString* path, + StorageCreationMode mode) { + auto directory = resolve_cache_directory(path, mode); + if (!directory.ok()) { + return directory.error(); } - auto parsed = parse_manifest(encoded); - if (!parsed.ok()) { - return parsed.error(); + if (!directory->has_value()) { + return Error::NotFound; } - return select_assets(parsed.get(), @"arch_b", @"macOS"); + return std::move(directory->value()); +} + +Result> acquire_test_cache_entry( + NSString* path, + NSString* key) { + auto directory = test_cache_directory(path); + if (!directory.ok()) { + return directory.error(); + } + return LockedCacheEntry::acquire(directory.get(), key); +} + +Result> acquire_existing_test_cache_entry( + NSString* path, + NSString* key) { + auto directory = + resolve_cache_directory(path, StorageCreationMode::OpenExisting); + if (!directory.ok()) { + return directory.error(); + } + if (!directory->has_value()) { + return std::unique_ptr(); + } + return LockedCacheEntry::acquire_existing(directory->value(), key); +} + +bool send_child_byte(int fd, char byte) { + return send_child_byte_impl(fd, byte); } Result test_cache_key(const SelectedBundle& bundle) { @@ -136,7 +169,7 @@ bool send_signal_byte(int fd, char byte) { } Result saved_bookmark(NSURL* root, NSString* key) { - auto locked = lock_cache_entry(root.path, key); + auto locked = acquire_test_cache_entry(root.path, key); if (!locked.ok()) { return locked.error(); } @@ -212,15 +245,15 @@ int bookmark_child_mode(int argc, char** argv) { scope.observe = ^(FaultPoint point) { if (lock && (same || collision) && point == FaultPoint::BeforeCacheEntryLock && - !send_signal_byte(notify_fd, 'B')) { + !send_child_byte_impl(notify_fd, 'B')) { _exit(12); } }; - auto locked = lock_cache_entry(root, selected_key); + auto locked = acquire_test_cache_entry(root, selected_key); if (!locked.ok()) { return 9; } - if (!send_signal_byte(signal_fd.get(), 'L')) { + if (!send_child_byte_impl(signal_fd.get(), 'L')) { return 10; } if (close(signal_fd.release()) != 0) { diff --git a/backends/apple/coreai/runtime/test/coreai_bookmark_test.mm b/backends/apple/coreai/runtime/test/coreai_bookmark_test.mm index 32c002d1b5e..974a12a02b3 100644 --- a/backends/apple/coreai/runtime/test/coreai_bookmark_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_bookmark_test.mm @@ -44,7 +44,7 @@ void SetUp() override { class CoreAIBookmarkIOTest : public CoreAIBookmarkPathTest { protected: - std::unique_ptr locked; + std::unique_ptr locked; NSURL* url = nil; void SetUp() override { @@ -52,7 +52,7 @@ void SetUp() override { if (HasFatalFailure()) { return; } - auto result = lock_cache_entry(root.url.path, key); + auto result = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(result.ok()); locked = std::move(result.get()); ASSERT_NE(locked, nullptr); @@ -62,7 +62,7 @@ void SetUp() override { }; ::testing::AssertionResult bookmark_bytes_equal( - const CacheEntryLock& lock, + const LockedCacheEntry& lock, NSData* expected) { auto result = lock.read_bookmark(); if (!result.ok()) { @@ -187,7 +187,7 @@ FileDescriptor file(open( NSData* bytes = [@"bookmark" dataUsingEncoding:NSUTF8StringEncoding]; ASSERT_EQ(locked->write_bookmark(bytes), Error::Ok); locked.reset(); - auto existing = lock_existing_cache_entry(root.url.path, key); + auto existing = acquire_existing_test_cache_entry(root.url.path, key); ASSERT_TRUE(existing.ok()); ASSERT_NE(existing.get(), nullptr); EXPECT_EQ( @@ -199,17 +199,19 @@ FileDescriptor file(open( TEST_F(CoreAIBookmarkIOTest, PreparesSharedStagingParentOnDemand) { EXPECT_TRUE([locked->key() isEqual:key]); - NSString* expected = [root.url URLByAppendingPathComponent:@"staging"].path; + NSString* expected = [root.url.URLByResolvingSymlinksInPath + URLByAppendingPathComponent:@"staging"] + .path; EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:expected]); - auto staging = locked->prepare_staging_root(); + auto staging = locked->prepare_staging_directory(); ASSERT_TRUE(staging.ok()); - EXPECT_TRUE([staging.get() isEqual:expected]); + EXPECT_TRUE([staging->url.path isEqual:expected]); struct stat info {}; - ASSERT_EQ(lstat(staging.get().fileSystemRepresentation, &info), 0); + ASSERT_EQ(lstat(staging->url.fileSystemRepresentation, &info), 0); EXPECT_TRUE(S_ISDIR(info.st_mode)); - auto repeated = locked->prepare_staging_root(); + auto repeated = locked->prepare_staging_directory(); ASSERT_TRUE(repeated.ok()); - EXPECT_TRUE([repeated.get() isEqual:expected]); + EXPECT_TRUE([repeated->url.path isEqual:expected]); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:[expected stringByAppendingPathComponent:key]]); } @@ -223,9 +225,9 @@ FileDescriptor file(open( } TEST_F(CoreAIBookmarkIOTest, CleanupRejectsUnexpectedTypesBeforeDeletingFiles) { - auto staging = locked->prepare_staging_root(); + auto staging = locked->prepare_staging_directory(); ASSERT_TRUE(staging.ok()); - NSString* entry = [staging.get() stringByAppendingPathComponent:key]; + NSString* entry = [staging->url.path stringByAppendingPathComponent:key]; ASSERT_EQ(mkdir(entry.fileSystemRepresentation, 0700), 0); NSString* file = [entry stringByAppendingPathComponent:@"data"]; NSData* bytes = [@"preserved" dataUsingEncoding:NSUTF8StringEncoding]; @@ -241,9 +243,9 @@ FileDescriptor file(open( } TEST_F(CoreAIBookmarkIOTest, CleanupUsesLockedRootAfterPathReplacement) { - auto staging = locked->prepare_staging_root(); + auto staging = locked->prepare_staging_directory(); ASSERT_TRUE(staging.ok()); - NSString* entry = [staging.get() stringByAppendingPathComponent:key]; + NSString* entry = [staging->url.path stringByAppendingPathComponent:key]; ASSERT_EQ(mkdir(entry.fileSystemRepresentation, 0700), 0); TestDirectory moved; ASSERT_NE(moved.url, nil); @@ -271,7 +273,7 @@ FileDescriptor file(open( ASSERT_EQ(mkdir(reference.fileSystemRepresentation, 0777), 0); struct stat expected; ASSERT_EQ(stat(reference.fileSystemRepresentation, &expected), 0); - ASSERT_TRUE(locked->prepare_staging_root().ok()); + ASSERT_TRUE(locked->prepare_staging_directory().ok()); for (NSString* name in @[ @"locks", @"bookmarks", @"staging" ]) { NSURL* directory = [root.url URLByAppendingPathComponent:name]; struct stat actual; @@ -281,35 +283,35 @@ FileDescriptor file(open( } TEST_F(CoreAIBookmarkIOTest, ReusesDirectoriesWithoutCreation) { - ASSERT_TRUE(locked->prepare_staging_root().ok()); + ASSERT_TRUE(locked->prepare_staging_directory().ok()); ASSERT_EQ( locked->write_bookmark( [@"bookmark" dataUsingEncoding:NSUTF8StringEncoding]), Error::Ok); locked.reset(); StorageFaultScope fault(FaultPoint::BeforeCreateDirectory); - auto created = lock_cache_entry(root.url.path, key); + auto created = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(created.ok()); ASSERT_NE(created.get(), nullptr); - EXPECT_TRUE(created.get()->prepare_staging_root().ok()); + EXPECT_TRUE(created.get()->prepare_staging_directory().ok()); created.get().reset(); - auto existing = lock_existing_cache_entry(root.url.path, key); + auto existing = acquire_existing_test_cache_entry(root.url.path, key); ASSERT_TRUE(existing.ok()); ASSERT_NE(existing.get(), nullptr); - EXPECT_TRUE(existing.get()->prepare_staging_root().ok()); + EXPECT_TRUE(existing.get()->prepare_staging_directory().ok()); EXPECT_EQ(fault.matches, 0); } TEST_F(CoreAIBookmarkIOTest, RecreatesMissingStagingParent) { - auto staging = locked->prepare_staging_root(); + auto staging = locked->prepare_staging_directory(); ASSERT_TRUE(staging.ok()); - ASSERT_EQ(rmdir(staging.get().fileSystemRepresentation), 0); + ASSERT_EQ(rmdir(staging->url.fileSystemRepresentation), 0); StorageFaultScope fault(FaultPoint::BeforeCreateDirectory); - EXPECT_EQ(locked->prepare_staging_root().error(), Error::AccessFailed); + EXPECT_EQ(locked->prepare_staging_directory().error(), Error::AccessFailed); EXPECT_EQ(fault.hits, 1); - auto recreated = locked->prepare_staging_root(); + auto recreated = locked->prepare_staging_directory(); ASSERT_TRUE(recreated.ok()); - EXPECT_TRUE([recreated.get() isEqual:staging.get()]); + EXPECT_TRUE([recreated->url isEqual:staging->url]); } TEST_F(CoreAIBookmarkIOTest, AcceptsConcurrentStagingCreation) { @@ -323,7 +325,7 @@ FileDescriptor file(open( EXPECT_EQ(mkdir(staging.fileSystemRepresentation, 0700), 0); } }; - EXPECT_TRUE(locked->prepare_staging_root().ok()); + EXPECT_TRUE(locked->prepare_staging_directory().ok()); EXPECT_EQ(creations, 1); } @@ -336,43 +338,45 @@ FileDescriptor file(open( target.url.fileSystemRepresentation, staging.fileSystemRepresentation), 0); - EXPECT_EQ(locked->prepare_staging_root().error(), Error::AccessFailed); + EXPECT_EQ(locked->prepare_staging_directory().error(), Error::AccessFailed); ASSERT_EQ(unlink(staging.fileSystemRepresentation), 0); NSData* bytes = [@"not a directory" dataUsingEncoding:NSUTF8StringEncoding]; ASSERT_TRUE([bytes writeToURL:staging atomically:NO]); - EXPECT_EQ(locked->prepare_staging_root().error(), Error::AccessFailed); + EXPECT_EQ(locked->prepare_staging_directory().error(), Error::AccessFailed); EXPECT_TRUE([[NSData dataWithContentsOfURL:staging] isEqual:bytes]); } TEST_F(CoreAIBookmarkPathTest, AllowsTemporaryRootsAndRejectsInvalidKeys) { - auto allowed = lock_cache_entry(root.url.path, key); + auto allowed = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(allowed.ok()); ASSERT_NE(allowed.get(), nullptr); - EXPECT_FALSE(lock_cache_entry(root.url.path, @"../escape").ok()); + EXPECT_FALSE(acquire_test_cache_entry(root.url.path, @"../escape").ok()); } TEST_F(CoreAIBookmarkPathTest, SnapshotsMutableRootAtAcquisition) { TestDirectory other; ASSERT_NE(other.url, nil); NSMutableString* path = [root.url.path mutableCopy]; - auto locked = lock_cache_entry(path, key); + auto locked = acquire_test_cache_entry(path, key); ASSERT_TRUE(locked.ok()); ASSERT_NE(locked.get(), nullptr); [path setString:other.url.path]; - auto staging = locked.get()->prepare_staging_root(); + auto staging = locked.get()->prepare_staging_directory(); ASSERT_TRUE(staging.ok()); - NSString* expected = [root.url URLByAppendingPathComponent:@"staging"].path; + NSString* expected = [root.url.URLByResolvingSymlinksInPath + URLByAppendingPathComponent:@"staging"] + .path; NSString* unrelated = [other.url URLByAppendingPathComponent:@"staging"].path; EXPECT_TRUE([NSFileManager.defaultManager fileExistsAtPath:expected]); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:unrelated]); - EXPECT_TRUE([staging.get() isEqual:expected]) - << "Returned staging path: " << staging.get().UTF8String + EXPECT_TRUE([staging->url.path isEqual:expected]) + << "Returned staging path: " << staging->url.path.UTF8String << "; descriptor-pinned staging path: " << expected.UTF8String; } TEST_F(CoreAIBookmarkPathTest, ExcludesRootWithoutCreatingStaging) { - auto locked = lock_cache_entry(root.url.path, key); + auto locked = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(locked.ok()); EXPECT_TRUE(backup_excluded(root.url)); EXPECT_FALSE([NSFileManager.defaultManager @@ -385,10 +389,16 @@ FileDescriptor file(open( for (NSString* path in @[ root.url.path, missing ]) { SCOPED_TRACE(path.UTF8String); auto resolved = - resolve_bookmark_root(path, StorageCreationMode::OpenExisting); + resolve_cache_directory(path, StorageCreationMode::OpenExisting); ASSERT_TRUE(resolved.ok()); - EXPECT_TRUE([resolved.get() isEqual:path]); - auto absent = lock_existing_cache_entry(path, key); + const bool root_exists = [path isEqual:root.url.path]; + EXPECT_EQ(resolved->has_value(), root_exists); + if (resolved->has_value()) { + EXPECT_TRUE([resolved->value().path + isEqual:[NSURL fileURLWithPath:path] + .URLByResolvingSymlinksInPath.path]); + } + auto absent = acquire_existing_test_cache_entry(path, key); ASSERT_TRUE(absent.ok()); EXPECT_EQ(absent.get(), nullptr); } @@ -403,11 +413,11 @@ FileDescriptor file(open( TEST_F(CoreAIBookmarkPathTest, MissingLookupIgnoresOtherEntries) { auto other_key = different_cache_key(*bundle, key, false); ASSERT_TRUE(other_key.ok()); - auto other = lock_cache_entry(root.url.path, other_key.get()); + auto other = acquire_test_cache_entry(root.url.path, other_key.get()); ASSERT_TRUE(other.ok()); NSData* bytes = [@"other" dataUsingEncoding:NSUTF8StringEncoding]; ASSERT_EQ(other.get()->write_bookmark(bytes), Error::Ok); - auto absent = lock_existing_cache_entry(root.url.path, key); + auto absent = acquire_existing_test_cache_entry(root.url.path, key); ASSERT_TRUE(absent.ok()); EXPECT_EQ(absent.get(), nullptr); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:lock_url().path]); @@ -421,7 +431,7 @@ FileDescriptor file(open( withIntermediateDirectories:YES attributes:nil error:nil]); - auto existing = lock_existing_cache_entry(root.url.path, key); + auto existing = acquire_existing_test_cache_entry(root.url.path, key); ASSERT_TRUE(existing.ok()); ASSERT_NE(existing.get(), nullptr); EXPECT_EQ(existing.get()->persistence(), CacheEntryPersistence::StagingOnly); @@ -436,16 +446,20 @@ FileDescriptor file(open( TEST_F(CoreAIBookmarkPathTest, ResolvesAndCreatesRootWithBackupExclusion) { ASSERT_TRUE(set_backup_excluded(root.url, false)); - NSString* path = [root.url URLByAppendingPathComponent:@"nested/assets"].path; - auto resolved = resolve_bookmark_root(path, StorageCreationMode::Create); + NSString* path = [root.url URLByAppendingPathComponent:@"nested/cache"].path; + auto resolved = resolve_cache_directory(path, StorageCreationMode::Create); ASSERT_TRUE(resolved.ok()); - EXPECT_TRUE([resolved.get() isEqual:path]); - EXPECT_TRUE(backup_excluded([NSURL fileURLWithPath:resolved.get()])); + ASSERT_TRUE(resolved->has_value()); + EXPECT_TRUE([resolved->value().path + isEqual:[NSURL fileURLWithPath:path].URLByResolvingSymlinksInPath.path]); + EXPECT_TRUE(backup_excluded([NSURL fileURLWithPath:resolved->value().path])); EXPECT_FALSE(backup_excluded(root.url)); auto existing = - resolve_bookmark_root(path, StorageCreationMode::OpenExisting); + resolve_cache_directory(path, StorageCreationMode::OpenExisting); ASSERT_TRUE(existing.ok()); - EXPECT_TRUE([existing.get() isEqual:path]); + ASSERT_TRUE(existing->has_value()); + EXPECT_TRUE([existing->value().path + isEqual:[NSURL fileURLWithPath:path].URLByResolvingSymlinksInPath.path]); } TEST_F(CoreAIBookmarkPathTest, RejectsInvalidResolvedRoots) { @@ -455,18 +469,19 @@ FileDescriptor file(open( @[ @"relative", @"~", @"~/cache", @"~nobody/cache" ]) { SCOPED_TRACE(path.UTF8String); EXPECT_EQ( - resolve_bookmark_root(path, mode).error(), Error::InvalidArgument); + resolve_cache_directory(path, mode).error(), Error::InvalidArgument); } - EXPECT_EQ(resolve_bookmark_root(nil, mode).error(), Error::InvalidArgument); + EXPECT_EQ( + resolve_cache_directory(nil, mode).error(), Error::InvalidArgument); } NSURL* file = [root.url URLByAppendingPathComponent:@"file"]; ASSERT_TRUE([[NSData data] writeToURL:file atomically:NO]); EXPECT_EQ( - resolve_bookmark_root(file.path, StorageCreationMode::OpenExisting) + resolve_cache_directory(file.path, StorageCreationMode::OpenExisting) .error(), Error::AccessFailed); EXPECT_EQ( - resolve_bookmark_root( + resolve_cache_directory( [file.path stringByAppendingPathComponent:@"child"], StorageCreationMode::OpenExisting) .error(), @@ -494,7 +509,8 @@ FileDescriptor file(open( ASSERT_EQ(mkfifo(stripe.fileSystemRepresentation, 0600), 0); } EXPECT_EQ( - lock_cache_entry(isolated.url.path, key).error(), Error::AccessFailed); + acquire_test_cache_entry(isolated.url.path, key).error(), + Error::AccessFailed); EXPECT_TRUE([[NSData dataWithContentsOfURL:target] isEqual:bytes]); } } diff --git a/backends/apple/coreai/runtime/test/coreai_cache_key_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_key_test.mm index 368e0fc6fa4..19206651539 100644 --- a/backends/apple/coreai/runtime/test/coreai_cache_key_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_key_test.mm @@ -19,14 +19,6 @@ namespace { -Result selected_bundle(NSMutableDictionary* dictionary) { - auto manifest = parse_manifest(encode(dictionary)); - if (!manifest.ok()) { - return manifest.error(); - } - return select_assets(manifest.get(), @"arch_b", @"macOS"); -} - NSString* key_for_device(NSString* device) { NSString* model = [@"" stringByPaddingToLength:64 withString:@"a" @@ -44,7 +36,7 @@ ASSERT_TRUE(expected.ok()); auto dictionary = manifest_dict(); - dictionary[@"hash"] = @"cd"; + dictionary[@"export_hash"] = @"cd"; dictionary[@"path"] = @"cd/model.aimodel"; dictionary[@"function"] = @"another_function"; dictionary[@"input_names"] = @[ @"different_input" ]; @@ -85,17 +77,25 @@ }; auto manifest = parse_manifest(encode(dictionary)); ASSERT_TRUE(manifest.ok()); - auto first = select_assets(manifest.get(), @"arch_a", @"macOS"); - auto second = select_assets(manifest.get(), @"arch_b", @"macOS"); + auto first = select_bundle(manifest.get(), @"arch_a", @"macOS"); + auto second = select_bundle(manifest.get(), @"arch_b", @"macOS"); ASSERT_TRUE(first.ok()); ASSERT_TRUE(second.ok()); - auto first_key = cache_model_key(first.get()); - auto second_key = cache_model_key(second.get()); + auto first_key = bundle_identity(first.get()); + auto second_key = bundle_identity(second.get()); ASSERT_TRUE(first_key.ok()); ASSERT_TRUE(second_key.ok()); EXPECT_FALSE([first_key.get() isEqual:second_key.get()]); } +TEST(CoreAICacheKeyIdentityTest, RejectsUnknownBundleName) { + auto manifest = parse_manifest(encode(manifest_dict())); + ASSERT_TRUE(manifest.ok()); + EXPECT_EQ( + bundle_identity(manifest.get(), @"unknown.aimodel").error(), + Error::InvalidArgument); +} + TEST(CoreAICacheKeyIdentityTest, SeparatesDeviceAndSpecializationIdentity) { auto bundle = bookmark_bundle(); ASSERT_TRUE(bundle.ok()); @@ -113,8 +113,8 @@ for (auto* changed : {&platform, &architecture, &specialization}) { ASSERT_TRUE(changed->ok()); EXPECT_FALSE([changed->get() isEqual:baseline.get()]); - EXPECT_TRUE([cache_key_model_prefix(changed->get()) - isEqual:cache_key_model_prefix(baseline.get())]); + EXPECT_TRUE([cache_key_bundle_identity(changed->get()) + isEqual:cache_key_bundle_identity(baseline.get())]); } } @@ -135,7 +135,7 @@ NSString* key = key_for_device(device); EXPECT_TRUE(is_cache_key(key)); EXPECT_TRUE( - [cache_key_model_prefix(key) isEqual:[key substringToIndex:64]]); + [cache_key_bundle_identity(key) isEqual:[key substringToIndex:64]]); } } @@ -153,7 +153,7 @@ ]) { NSString* key = key_for_device(device); EXPECT_FALSE(is_cache_key(key)); - EXPECT_EQ(cache_key_model_prefix(key), nil); + EXPECT_EQ(cache_key_bundle_identity(key), nil); } } diff --git a/backends/apple/coreai/runtime/test/coreai_cache_root_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_root_test.mm index b6cf8141065..04235d45a83 100644 --- a/backends/apple/coreai/runtime/test/coreai_cache_root_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_root_test.mm @@ -18,6 +18,27 @@ using namespace executorch::backends::coreai; using namespace executorch::backends::coreai::testing; +TEST( + CoreAICacheRootTest, + ResolvesDefaultAndExplicitPathsWithoutCreatingStorage) { + auto default_path = cache_directory_path(nullptr); + ASSERT_TRUE(default_path.ok()); + EXPECT_TRUE(default_path.get().isAbsolutePath); + EXPECT_TRUE( + [default_path.get().lastPathComponent isEqual:@"executorch_coreai"]); + + TestDirectory storage; + ASSERT_NE(storage.url, nil); + NSURL* missing = [storage.url URLByAppendingPathComponent:@"missing"]; + auto explicit_path = cache_directory_path(missing.path.UTF8String); + ASSERT_TRUE(explicit_path.ok()); + EXPECT_TRUE([explicit_path.get() isEqual:missing.path]); + EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:missing.path]); + for (const char* invalid : {"", "relative", "~", "~/cache", "\xff"}) { + EXPECT_EQ(cache_directory_path(invalid).error(), Error::InvalidArgument); + } +} + TEST(CoreAICacheRootTest, PreparesRootWithRootOnlyBackupPolicy) { NSFileManager* manager = NSFileManager.defaultManager; TestDirectory sandbox; @@ -30,29 +51,36 @@ ASSERT_TRUE(set_backup_excluded(sandbox.url, false)); ASSERT_TRUE(set_backup_excluded(parent, false)); NSURL* root = [parent URLByAppendingPathComponent:@"nested/models"]; - auto prepared = prepare_storage_root(root.path, StorageCreationMode::Create); - ASSERT_TRUE((prepared.ok() && [prepared.get() isEqualToString:root.path])); + auto prepared = + resolve_cache_directory(root.path, StorageCreationMode::Create); + ASSERT_TRUE(prepared.ok()); + ASSERT_TRUE(prepared->has_value()); + EXPECT_TRUE([prepared->value().path + isEqualToString:root.URLByResolvingSymlinksInPath.path]); ASSERT_TRUE((backup_excluded(root))); ASSERT_TRUE((!backup_excluded(parent) && !backup_excluded(sandbox.url))); ASSERT_TRUE((!backup_excluded(root.URLByDeletingLastPathComponent))); ASSERT_TRUE(set_backup_excluded(root, false)); auto inspected = - prepare_storage_root(root.path, StorageCreationMode::OpenExisting); - ASSERT_TRUE((inspected.ok() && [inspected.get() isEqualToString:root.path])); + resolve_cache_directory(root.path, StorageCreationMode::OpenExisting); + ASSERT_TRUE(inspected.ok()); + ASSERT_TRUE(inspected->has_value()); + EXPECT_TRUE([inspected->value().path + isEqualToString:root.URLByResolvingSymlinksInPath.path]); ASSERT_TRUE((!backup_excluded(root))); ASSERT_TRUE( - (prepare_storage_root(root.path, StorageCreationMode::Create).ok() && + (resolve_cache_directory(root.path, StorageCreationMode::Create).ok() && backup_excluded(root))); NSURL* missing = [sandbox.url URLByAppendingPathComponent:@"missing"]; - ASSERT_EQ( - prepare_storage_root(missing.path, StorageCreationMode::OpenExisting) - .error(), - Error::InvalidArgument); + auto absent = + resolve_cache_directory(missing.path, StorageCreationMode::OpenExisting); + ASSERT_TRUE(absent.ok()); + EXPECT_FALSE(absent->has_value()); EXPECT_FALSE([manager fileExistsAtPath:missing.path]); EXPECT_EQ( - prepare_storage_root(@"relative", StorageCreationMode::Create).error(), + resolve_cache_directory(@"relative", StorageCreationMode::Create).error(), Error::InvalidArgument); EXPECT_TRUE((!backup_excluded(parent) && !backup_excluded(sandbox.url))); } @@ -62,28 +90,30 @@ ASSERT_NE(storage.url, nil); NSURL* root = [storage.url URLByAppendingPathComponent:@"cache"]; ASSERT_TRUE( - prepare_storage_root(root.path, StorageCreationMode::Create).ok()); + resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); StorageFaultScope fault(FaultPoint::BeforeCreateDirectory); EXPECT_TRUE( - prepare_storage_root(root.path, StorageCreationMode::Create).ok()); + resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); EXPECT_TRUE( - prepare_storage_root(root.path, StorageCreationMode::OpenExisting).ok()); + resolve_cache_directory(root.path, StorageCreationMode::OpenExisting) + .ok()); EXPECT_EQ(fault.matches, 0); EXPECT_TRUE(backup_excluded(root)); NSURL* missing = [storage.url URLByAppendingPathComponent:@"missing"]; - EXPECT_EQ( - prepare_storage_root(missing.path, StorageCreationMode::OpenExisting) - .error(), - Error::InvalidArgument); + auto absent = + resolve_cache_directory(missing.path, StorageCreationMode::OpenExisting); + ASSERT_TRUE(absent.ok()); + EXPECT_FALSE(absent->has_value()); EXPECT_EQ(fault.matches, 0); EXPECT_EQ( - prepare_storage_root(missing.path, StorageCreationMode::Create).error(), + resolve_cache_directory(missing.path, StorageCreationMode::Create) + .error(), Error::AccessFailed); EXPECT_EQ(fault.hits, 1); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:missing.path]); EXPECT_TRUE( - prepare_storage_root(missing.path, StorageCreationMode::Create).ok()); + resolve_cache_directory(missing.path, StorageCreationMode::Create).ok()); } TEST(CoreAICacheRootTest, CreatesDirectoriesWithDefaultPermissions) { @@ -95,7 +125,7 @@ ASSERT_EQ(stat(reference.fileSystemRepresentation, &expected), 0); NSURL* root = [storage.url URLByAppendingPathComponent:@"cache"]; ASSERT_TRUE( - prepare_storage_root(root.path, StorageCreationMode::Create).ok()); + resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); FileDescriptor directory( open(root.fileSystemRepresentation, O_RDONLY | O_DIRECTORY | O_CLOEXEC)); ASSERT_GE(directory.get(), 0); @@ -114,10 +144,10 @@ FileDescriptor directory( ASSERT_NE(storage.url, nil); NSURL* root = [storage.url URLByAppendingPathComponent:@"cache"]; ASSERT_TRUE( - prepare_storage_root(root.path, StorageCreationMode::Create).ok()); + resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); ASSERT_EQ(rmdir(root.fileSystemRepresentation), 0); ASSERT_TRUE( - prepare_storage_root(root.path, StorageCreationMode::Create).ok()); + resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); EXPECT_TRUE(backup_excluded(root)); } @@ -126,7 +156,7 @@ FileDescriptor directory( ASSERT_NE(sandbox.url, nil); NSURL* root = [sandbox.url URLByAppendingPathComponent:@"cache"]; ASSERT_TRUE( - prepare_storage_root(root.path, StorageCreationMode::Create).ok()); + resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); FileDescriptor directory( open_directory_at(AT_FDCWD, root.fileSystemRepresentation)); ASSERT_GE(directory.get(), 0); @@ -171,7 +201,7 @@ FileDescriptor directory( ASSERT_TRUE([before[NSFileProtectionKey] isEqual:NSFileProtectionComplete]); for (StorageCreationMode mode : {StorageCreationMode::OpenExisting, StorageCreationMode::Create}) { - ASSERT_TRUE(prepare_storage_root(root.path, mode).ok()); + ASSERT_TRUE(resolve_cache_directory(root.path, mode).ok()); NSDictionary* after = [manager attributesOfItemAtPath:root.path error:&error]; ASSERT_NE(after, nil) << error.description.UTF8String; @@ -179,6 +209,44 @@ FileDescriptor directory( } } +TEST(CoreAICacheRootTest, SupportsSymlinkAndAnchorsResolvedDirectory) { + TestDirectory storage; + ASSERT_NE(storage.url, nil); + NSURL* first = [storage.url URLByAppendingPathComponent:@"first"]; + NSURL* second = [storage.url URLByAppendingPathComponent:@"second"]; + NSURL* configured = [storage.url URLByAppendingPathComponent:@"configured"]; + ASSERT_EQ(mkdir(first.fileSystemRepresentation, 0777), 0); + ASSERT_EQ(mkdir(second.fileSystemRepresentation, 0777), 0); + ASSERT_EQ( + symlink( + first.fileSystemRepresentation, configured.fileSystemRepresentation), + 0); + + auto resolved = + resolve_cache_directory(configured.path, StorageCreationMode::Create); + ASSERT_TRUE(resolved.ok()); + ASSERT_TRUE(resolved->has_value()); + struct stat target_info; + struct stat resolved_info; + ASSERT_EQ(stat(first.fileSystemRepresentation, &target_info), 0); + ASSERT_EQ(fstat(resolved->value().descriptor.get(), &resolved_info), 0); + EXPECT_EQ(resolved_info.st_ino, target_info.st_ino); + + ASSERT_EQ(unlink(configured.fileSystemRepresentation), 0); + ASSERT_EQ( + symlink( + second.fileSystemRepresentation, configured.fileSystemRepresentation), + 0); + ASSERT_EQ( + write_storage_file( + resolved->value().descriptor.get(), @"anchored", "x", 1), + Error::Ok); + EXPECT_TRUE([NSFileManager.defaultManager + fileExistsAtPath:[first URLByAppendingPathComponent:@"anchored"].path]); + EXPECT_FALSE([NSFileManager.defaultManager + fileExistsAtPath:[second URLByAppendingPathComponent:@"anchored"].path]); +} + TEST(CoreAICacheRootTest, RejectsInvalidRootsConsistently) { TestDirectory storage; ASSERT_NE(storage.url, nil); @@ -188,19 +256,22 @@ FileDescriptor directory( for (StorageCreationMode mode : {StorageCreationMode::OpenExisting, StorageCreationMode::Create}) { SCOPED_TRACE(static_cast(mode)); - EXPECT_EQ(prepare_storage_root(nil, mode).error(), Error::InvalidArgument); + EXPECT_EQ( + resolve_cache_directory(nil, mode).error(), Error::InvalidArgument); + for (NSString* path in + @[ @"", @"relative", @"~", @"~/cache", @"~nobody/cache" ]) { + SCOPED_TRACE(path.UTF8String); + EXPECT_EQ( + resolve_cache_directory(path, mode).error(), Error::InvalidArgument); + } for (NSString* path in @[ - @"", - @"relative", - @"~", - @"~/cache", - @"~nobody/cache", regular.path, [regular.path stringByAppendingPathComponent:@"child"] ]) { - SCOPED_TRACE(path.UTF8String); EXPECT_EQ( - prepare_storage_root(path, mode).error(), Error::InvalidArgument); + resolve_cache_directory(path, mode).error(), + mode == StorageCreationMode::Create ? Error::InvalidArgument + : Error::AccessFailed); } } EXPECT_TRUE([[NSData dataWithContentsOfURL:regular] isEqual:bytes]); diff --git a/backends/apple/coreai/runtime/test/coreai_cache_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_test.mm index 0d8f732e2ee..8af4292d21a 100644 --- a/backends/apple/coreai/runtime/test/coreai_cache_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_test.mm @@ -24,6 +24,7 @@ #include "coreai_filesystem_fixture.h" #include "coreai_pte_fixture.h" #include "coreai_source_fixture.h" +#include "coreai_test_utils.h" namespace executorch::backends::coreai::testing { namespace { @@ -41,8 +42,7 @@ void SetUp() override { } void drain_callbacks() { - auto drained = bridge.wait_for_callbacks( - dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); + auto drained = bridge.wait_for_callbacks(test_deadline()); if (!drained) { ADD_FAILURE() << drained.message(); // Timed-out callbacks may still borrow this test's PTE or observations. @@ -76,7 +76,7 @@ AssertionResult seed_cache( if (!parsed.ok()) { return AssertionFailure() << "Cannot parse cache seed manifest"; } - auto selected = select_assets(parsed.get(), architecture, platform); + auto selected = select_bundle(parsed.get(), architecture, platform); if (!selected.ok()) { return AssertionFailure() << "Cannot select cache seed assets"; } @@ -90,7 +90,7 @@ AssertionResult seed_cache( nil, [[root URLByAppendingPathComponent:@"staging"] URLByAppendingPathComponent:key.get()]}; - auto lock = lock_cache_entry(root.path, entry.key); + auto lock = acquire_test_cache_entry(root.path, entry.key); if (!lock.ok()) { return AssertionFailure() << "Cannot lock cache seed bookmark"; } diff --git a/backends/apple/coreai/runtime/test/coreai_delegate_fixture.mm b/backends/apple/coreai/runtime/test/coreai_delegate_fixture.mm index 6cac71e0c4f..3d450ee4d24 100644 --- a/backends/apple/coreai/runtime/test/coreai_delegate_fixture.mm +++ b/backends/apple/coreai/runtime/test/coreai_delegate_fixture.mm @@ -6,6 +6,7 @@ * LICENSE file in the root directory of this source tree. */ #import "coreai_delegate_fixture.h" +#include "coreai_test_utils.h" namespace executorch::backends::coreai::testing { using namespace executorch::runtime; @@ -25,8 +26,7 @@ DelegateFixture::~DelegateFixture() { reset(); - EXPECT_TRUE(bridge_.wait_for_callbacks( - dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))); + EXPECT_TRUE(bridge_.wait_for_callbacks(test_deadline())); processed.Free(); if (json != nil) { EXPECT_EQ(freed, 1); @@ -40,7 +40,7 @@ } BackendOptions<1> options; const Error error = options.set_option( - "coreai_assets_dir", (root ?: storage.url.path).UTF8String); + "coreai_cache_dir", (root ?: storage.url.path).UTF8String); if (error != Error::Ok) { return error; } @@ -56,8 +56,7 @@ if (result.ok()) { handle = result.get(); } - auto drained = bridge_.wait_for_callbacks( - dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); + auto drained = bridge_.wait_for_callbacks(test_deadline()); if (!drained) { ADD_FAILURE() << drained.message(); reset(); diff --git a/backends/apple/coreai/runtime/test/coreai_delegate_test.mm b/backends/apple/coreai/runtime/test/coreai_delegate_test.mm index 8dcebeeafc7..afc117ae324 100644 --- a/backends/apple/coreai/runtime/test/coreai_delegate_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_delegate_test.mm @@ -10,11 +10,15 @@ #include #include #include +#include +#include #include #include #include "coreai_bookmark_fixture.h" +#include "coreai_cache.h" #import "coreai_delegate_fixture.h" +#include "coreai_test_utils.h" namespace executorch::backends::coreai::testing { namespace { @@ -165,7 +169,7 @@ void expect_preflight_rejection( bridge.state().device_architecture = arch; auto parsed = parse_manifest(fixture.json); ASSERT_TRUE(parsed.ok()); - auto selected = select_assets(parsed.get(), arch, @"macOS"); + auto selected = select_bundle(parsed.get(), arch, @"macOS"); ASSERT_TRUE(selected.ok()); ASSERT_EQ(fixture.init(), Error::Ok); auto key = cache_variant_key(selected.get(), @"macOS", arch, @"default"); @@ -204,7 +208,8 @@ void expect_preflight_rejection( EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:bridge.state().last_bundle]); EXPECT_TRUE([bridge.state().last_bundle - hasPrefix:[fixture.storage.url.path stringByAppendingString:@"/"]]); + hasPrefix:[fixture.storage.url.URLByResolvingSymlinksInPath.path + stringByAppendingString:@"/"]]); } EXPECT_EQ(bridge.state().sessions.load(), 0); } @@ -476,8 +481,7 @@ int start() { return error; } long wait_ready() { - return dispatch_semaphore_wait( - state_.input_ready, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)); + return dispatch_semaphore_wait(state_.input_ready, test_deadline(5)); } long wait_finished(int64_t timeout) { return dispatch_semaphore_wait( @@ -489,8 +493,7 @@ void release() { void release_and_join() { release(); if (started_) { - const long returned = dispatch_semaphore_wait( - returned_, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)); + const long returned = dispatch_semaphore_wait(returned_, test_deadline()); if (returned != 0) { ADD_FAILURE() << "Delegate worker did not return after releasing its input gate"; @@ -548,8 +551,7 @@ void release_and_join() { worker.release(); const long finished = worker.wait_finished(5 * NSEC_PER_SEC); worker.release_and_join(); - ASSERT_TRUE(bridge.wait_for_callbacks( - dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC))); + ASSERT_TRUE(bridge.wait_for_callbacks(test_deadline())); EXPECT_EQ(ready, 0); EXPECT_EQ(observed, tensors.input); EXPECT_NE(premature, 0); @@ -828,16 +830,15 @@ TensorImpl output( @autoreleasepool { DelegateFixture fixture(backend, bridge); { - SCOPED_TRACE("integer assets directory"); - BackendOption wrong_type{"coreai_assets_dir", 1}; + SCOPED_TRACE("integer cache directory"); + BackendOption wrong_type{"coreai_cache_dir", 1}; EXPECT_EQ( fixture.init_with_options({&wrong_type, 1}), Error::InvalidArgument); } { - SCOPED_TRACE("relative assets directory"); + SCOPED_TRACE("relative cache directory"); BackendOptions<1> relative; - ASSERT_EQ( - relative.set_option("coreai_assets_dir", "relative"), Error::Ok); + ASSERT_EQ(relative.set_option("coreai_cache_dir", "relative"), Error::Ok); EXPECT_EQ( fixture.init_with_options( {relative.view().data(), relative.view().size()}), @@ -847,6 +848,27 @@ TensorImpl output( } } +TEST_F(CoreAIDelegateTest, ConfiguredSymlinkCacheDirectoryLoadsAndClears) { + @autoreleasepool { + DelegateFixture fixture(backend, bridge); + NSURL* target = + [fixture.storage.url URLByAppendingPathComponent:@"cache-target"]; + NSURL* configured = + [fixture.storage.url URLByAppendingPathComponent:@"cache-link"]; + ASSERT_EQ(mkdir(target.fileSystemRepresentation, 0777), 0); + ASSERT_EQ( + symlink( + target.fileSystemRepresentation, + configured.fileSystemRepresentation), + 0); + ASSERT_EQ(fixture.init(configured.path), Error::Ok); + EXPECT_TRUE([bridge.state().last_bundle containsString:@"/cache-target/"]); + EXPECT_FALSE([bridge.state().last_bundle containsString:@"/cache-link/"]); + fixture.reset(); + EXPECT_EQ(clear_cache(configured.path.UTF8String), Error::Ok); + } +} + TEST_F( CoreAIDelegateTest, LifetimeDestroyAndWarmReloadIsolateRootsWithoutSource) { @@ -873,7 +895,8 @@ TensorImpl output( ASSERT_EQ(other.init(nested.path), Error::Ok); EXPECT_EQ(bridge.state().sessions.load(), 2); EXPECT_TRUE([bridge.state().last_bundle - hasPrefix:[other.storage.url.path stringByAppendingString:@"/"]]); + hasPrefix:[other.storage.url.URLByResolvingSymlinksInPath.path + stringByAppendingString:@"/"]]); EXPECT_FALSE([bridge.state().last_bundle isEqualToString:first_bundle]); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:first_bundle]); other.reset(); diff --git a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm index 2585d23f64d..44296beab6c 100644 --- a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm @@ -10,11 +10,13 @@ #include #include #include +#include "coreai_acquisition_fixture.h" #include "coreai_bookmark_fixture.h" #include "coreai_fake_loader.h" #include "coreai_fault_scope.h" #include "coreai_load_coordinator.h" #include "coreai_source_fixture.h" +#include "coreai_test_utils.h" using namespace executorch::runtime; using namespace executorch::backends::coreai; @@ -33,7 +35,7 @@ Error initialize_and_destroy( if (root == nil) return Error::InvalidArgument; BackendOptions<1> options; - auto error = options.set_option("coreai_assets_dir", root.UTF8String); + auto error = options.set_option("coreai_cache_dir", root.UTF8String); if (error != Error::Ok) return error; auto* backend = get_backend_class("CoreAIBackend"); @@ -56,10 +58,6 @@ BackendInitContext context( return Error::Ok; } -dispatch_time_t deadline() { - return dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC); -} - class CoreAIEvictionTest : public ::testing::Test { protected: ScopedFakeBridgeState bridge; @@ -93,7 +91,7 @@ void join() { release(); if (joined) return; - const auto completion_deadline = deadline(); + const auto completion_deadline = test_deadline(); if (eviction_started) eviction_wait = dispatch_semaphore_wait(eviction_done, completion_deadline); @@ -114,7 +112,7 @@ void join() { << ", " << loading_join; std::_Exit(EXIT_FAILURE); } - auto drained = bridge.wait_for_callbacks(deadline()); + auto drained = bridge.wait_for_callbacks(test_deadline()); if (!drained) { ADD_FAILURE() << drained.message(); std::_Exit(EXIT_FAILURE); @@ -222,7 +220,7 @@ - (void)clearRetainedState { EXPECT_FALSE( [NSFileManager.defaultManager fileExistsAtPath:source_url.path]); ASSERT_EQ( - clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); + clear_test_cache_entry(root.url.path, key, fake.loader), Error::Ok); EXPECT_NE(fake.loader.models[original], nil); EXPECT_EQ(fake.loader.models[saved], nil); EXPECT_FALSE( @@ -255,7 +253,7 @@ - (void)clearRetainedState { ASSERT_TRUE(key_result.ok()); NSString* key = key_result.get(); EXPECT_EQ( - clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); + clear_test_cache_entry(root.url.path, key, fake.loader), Error::Ok); EXPECT_EQ(fake.loader->evictions.load(), 0); // Retain staging after publication to exercise clearing's failure paths. { @@ -280,7 +278,7 @@ - (void)clearRetainedState { fake.loader.evictError = bookmark_test_error(EBUSY); fake.loader.restoreError = probe_error ? bookmark_test_error() : nil; EXPECT_NE( - clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); + clear_test_cache_entry(root.url.path, key, fake.loader), Error::Ok); auto retained = saved_bookmark(root.url, key); ASSERT_TRUE(retained.ok()); EXPECT_TRUE([retained.get() isEqual:saved]); @@ -292,7 +290,7 @@ - (void)clearRetainedState { { StorageFaultScope fault(FaultPoint::AfterEvict); EXPECT_NE( - clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); + clear_test_cache_entry(root.url.path, key, fake.loader), Error::Ok); EXPECT_EQ(fault.hits, 1); } auto retained = saved_bookmark(root.url, key); @@ -300,7 +298,7 @@ - (void)clearRetainedState { EXPECT_TRUE([retained.get() isEqual:saved]); fake.loader.evictError = bookmark_test_error(ENOENT); ASSERT_EQ( - clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); + clear_test_cache_entry(root.url.path, key, fake.loader), Error::Ok); auto absent = saved_bookmark(root.url, key); ASSERT_TRUE(absent.ok()); EXPECT_EQ(absent.get(), nil); @@ -313,14 +311,14 @@ - (void)clearRetainedState { { StorageFaultScope fault(FaultPoint::Remove); EXPECT_NE( - clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); + clear_test_cache_entry(root.url.path, key, fake.loader), Error::Ok); EXPECT_EQ(fault.hits, 1); auto before_remove = saved_bookmark(root.url, key); ASSERT_TRUE(before_remove.ok()); EXPECT_NE(before_remove.get(), nil); } ASSERT_EQ( - clear_bookmark_assets(root.url.path, key, fake.loader), Error::Ok); + clear_test_cache_entry(root.url.path, key, fake.loader), Error::Ok); auto removed = saved_bookmark(root.url, key); ASSERT_TRUE(removed.ok()); EXPECT_EQ(removed.get(), nil); @@ -347,20 +345,19 @@ - (void)clearRetainedState { // Scope teardown clears retained completions even after fatal test // failures. bridge.state().bookmark_loader = loader; - RuntimeAssets assets{selected.get(), @"macOS", @"arch_b"}; + SelectedHostBundle assets{selected.get(), @"macOS", @"arch_b"}; @autoreleasepool { - auto pinned = - acquire_bookmark_model(assets, &data, root.url.path, loader); + auto pinned = acquire_test_model(assets, &data, root.url.path, loader); ASSERT_TRUE(pinned.ok()); EXPECT_NE(loader.temporary, nil); - EXPECT_NE(clear_bookmark_assets(root.url.path, key, loader), Error::Ok); + EXPECT_NE(clear_test_cache_entry(root.url.path, key, loader), Error::Ok); auto saved = saved_bookmark(root.url, key); ASSERT_TRUE(saved.ok()); EXPECT_NE(saved.get(), nil); } EXPECT_EQ(loader.temporary, nil); EXPECT_NE(loader.retainedCompletion, nil); - ASSERT_EQ(clear_bookmark_assets(root.url.path, key, loader), Error::Ok); + ASSERT_EQ(clear_test_cache_entry(root.url.path, key, loader), Error::Ok); auto removed = saved_bookmark(root.url, key); ASSERT_TRUE(removed.ok()); EXPECT_EQ(removed.get(), nil); @@ -398,21 +395,22 @@ - (void)clearRetainedState { __block long gate_wait = -1; fake.loader.onEvict = ^(NSData*) { dispatch_semaphore_signal(deleting); - gate_wait = dispatch_semaphore_wait(proceed, deadline()); + gate_wait = dispatch_semaphore_wait(proceed, test_deadline()); }; Error eviction_error = Error::Internal; bool load_ok = false; auto evict = [&] { @autoreleasepool { - eviction_error = clear_bookmark_assets(root.url.path, key, fake.loader); + eviction_error = + clear_test_cache_entry(root.url.path, key, fake.loader); } dispatch_semaphore_signal(eviction_done); }; - RuntimeAssets assets{selected.get(), @"macOS", @"arch_b"}; + SelectedHostBundle assets{selected.get(), @"macOS", @"arch_b"}; auto load = [&] { @autoreleasepool { auto acquired = - acquire_bookmark_model(assets, &data, root.url.path, fake.loader); + acquire_test_model(assets, &data, root.url.path, fake.loader); load_ok = acquired.ok() && acquired.get() != nil; } dispatch_semaphore_signal(loading_observed); @@ -429,7 +427,7 @@ - (void)clearRetainedState { &evict); cleanup.eviction_started = eviction_start == 0; const long entered = cleanup.eviction_started - ? dispatch_semaphore_wait(deleting, deadline()) + ? dispatch_semaphore_wait(deleting, test_deadline()) : -1; int loading_start = -1; long premature_completion = -1; diff --git a/backends/apple/coreai/runtime/test/coreai_fake_loader.mm b/backends/apple/coreai/runtime/test/coreai_fake_loader.mm index 4edae0b9c71..ccfb8ee1780 100644 --- a/backends/apple/coreai/runtime/test/coreai_fake_loader.mm +++ b/backends/apple/coreai/runtime/test/coreai_fake_loader.mm @@ -8,6 +8,7 @@ #import "coreai_fake_loader.h" #include +#include "coreai_test_utils.h" using executorch::backends::coreai::testing::FakeBridgeState; @@ -84,7 +85,7 @@ void run_async(CoreAIFakeBridgeContext* context, void (^work)()) { } dispatch_time_t cleanup_deadline() { - return dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC); + return executorch::backends::coreai::testing::test_deadline(); } } // namespace diff --git a/backends/apple/coreai/runtime/test/coreai_manifest_fixture.h b/backends/apple/coreai/runtime/test/coreai_manifest_fixture.h index 68223c980a9..bf9af55c0bb 100644 --- a/backends/apple/coreai/runtime/test/coreai_manifest_fixture.h +++ b/backends/apple/coreai/runtime/test/coreai_manifest_fixture.h @@ -23,7 +23,7 @@ inline NSString* fixture_identity(char digit) { inline NSMutableDictionary* manifest_dict() { return [@{ @"packaging" : @"inline", - @"hash" : @"ab", + @"export_hash" : @"ab", @"path" : @"ab/model.aimodel", @"function" : @"main", @"input_names" : @[ @"input_0" ], @@ -60,4 +60,19 @@ inline NSData* encode(NSDictionary* dict) { return [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil]; } +inline runtime::Result selected_bundle( + NSDictionary* dict, + NSString* architecture = @"arch_b", + NSString* platform = @"macOS") { + NSData* encoded = encode(dict); + if (encoded == nil) { + return runtime::Error::InvalidExternalData; + } + auto manifest = parse_manifest(encoded); + if (!manifest.ok()) { + return manifest.error(); + } + return select_bundle(manifest.get(), architecture, platform); +} + } // namespace executorch::backends::coreai::testing diff --git a/backends/apple/coreai/runtime/test/coreai_manifest_test.mm b/backends/apple/coreai/runtime/test/coreai_manifest_test.mm index cd7d2d22f65..2e0fa5393e7 100644 --- a/backends/apple/coreai/runtime/test/coreai_manifest_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_manifest_test.mm @@ -170,7 +170,7 @@ auto parsed = parse_manifest(encode(dict)); ASSERT_TRUE(parsed.ok()); EXPECT_EQ(parsed->aot_compiled, GetParam()); - EXPECT_TRUE([parsed->model_hash isEqual:dict[@"hash"]]); + EXPECT_TRUE([parsed->export_hash isEqual:dict[@"export_hash"]]); EXPECT_TRUE([parsed->function isEqual:dict[@"function"]]); EXPECT_TRUE([parsed->inputs isEqual:dict[@"input_names"]]); EXPECT_TRUE([parsed->outputs isEqual:dict[@"output_names"]]); @@ -246,12 +246,12 @@ TEST_P(CoreAIManifestMetadataTest, RejectsInvalidHashes) { auto missing = manifest(); - [missing removeObjectForKey:@"hash"]; + [missing removeObjectForKey:@"export_hash"]; EXPECT_EQ(parse_manifest(encode(missing)).error(), Error::InvalidProgram); for (id hash in @[ @"", @"g", @"AB", @"../x", @"a/b", @1, NSNull.null ]) { SCOPED_TRACE([hash description].UTF8String); auto dict = manifest(); - dict[@"hash"] = hash; + dict[@"export_hash"] = hash; if (GetParam()) { dict[@"archs"] = @{ @"arch_a" : @@ -391,12 +391,12 @@ [parsed->architectures copy]; for (NSString* arch in @[ @"arch_a", @"arch_b" ]) { SCOPED_TRACE(arch.UTF8String); - auto selected = select_assets(parsed.get(), arch, @"macOS"); + auto selected = select_bundle(parsed.get(), arch, @"macOS"); ASSERT_TRUE(selected.ok()); EXPECT_TRUE([selected->path isEqualToString:dict[@"archs"][arch]]); EXPECT_EQ(selected->files.count, 2); NSString* bundle = selected->path.lastPathComponent; - EXPECT_TRUE([selected->model_hash isEqual:parsed->model_hash]); + EXPECT_TRUE([selected->export_hash isEqual:parsed->export_hash]); EXPECT_TRUE([selected->function isEqual:parsed->function]); EXPECT_TRUE([selected->inputs isEqual:parsed->inputs]); EXPECT_TRUE([selected->outputs isEqual:parsed->outputs]); @@ -420,10 +420,10 @@ ASSERT_TRUE(parsed.ok()); ASSERT_FALSE(parsed->aot_compiled); // Selection ignores the architecture and platform for source bundles. - auto selected = select_assets(parsed.get(), @"anything", @"elsewhere"); + auto selected = select_bundle(parsed.get(), @"anything", @"elsewhere"); ASSERT_TRUE(selected.ok()); EXPECT_TRUE([selected->path isEqualToString:dict[@"path"]]); - EXPECT_TRUE([selected->model_hash isEqual:parsed->model_hash]); + EXPECT_TRUE([selected->export_hash isEqual:parsed->export_hash]); EXPECT_TRUE([selected->function isEqual:parsed->function]); EXPECT_TRUE([selected->inputs isEqual:parsed->inputs]); EXPECT_TRUE([selected->outputs isEqual:parsed->outputs]); @@ -441,7 +441,7 @@ ASSERT_TRUE(parsed.ok()); [bytes setLength:0]; [original removeAllObjects]; - auto selected = select_assets(parsed.get(), @"arch_b", @"macOS"); + auto selected = select_bundle(parsed.get(), @"arch_b", @"macOS"); ASSERT_TRUE(selected.ok()); EXPECT_TRUE([selected->path isEqual:@"ab/model.arch_b.aimodelc"]); EXPECT_EQ(selected->files.count, 2); @@ -470,30 +470,31 @@ } TEST(CoreAIManifestTest, ValidationHelpersMatchParserRules) { - EXPECT_FALSE(nonempty_string(nil)); - EXPECT_FALSE(nonempty_string(@"")); - EXPECT_TRUE(nonempty_string(@"ab")); + EXPECT_FALSE(is_nonempty_string(nil)); + EXPECT_FALSE(is_nonempty_string(@"")); + EXPECT_TRUE(is_nonempty_string(@"ab")); const unichar characters[] = {'a', 0, 'b'}; NSString* embedded_nul = [NSString stringWithCharacters:characters length:3]; ASSERT_EQ(embedded_nul.length, 3); - EXPECT_FALSE(nonempty_string(embedded_nul)); - EXPECT_FALSE(relative_path(embedded_nul)); - EXPECT_FALSE(lowercase_hex(embedded_nul)); - EXPECT_FALSE(nonempty_string(@1)); - EXPECT_FALSE(lowercase_hex((id) @1, 64)); - EXPECT_FALSE(lowercase_hex((id)NSNull.null, 64)); - EXPECT_FALSE(relative_path(@"/ab/model.aimodel")); - EXPECT_FALSE(relative_path(@"ab/../model.aimodel")); - EXPECT_FALSE(relative_path(@"a\\b")); - EXPECT_FALSE(relative_path(@"a//b")); - EXPECT_TRUE(relative_path(@"ab/model.aimodel")); - EXPECT_TRUE(lowercase_hex(@"ab")); - EXPECT_FALSE(lowercase_hex(@"")); - EXPECT_FALSE(lowercase_hex(@"AB")); - EXPECT_FALSE(lowercase_hex(@"ag")); - EXPECT_TRUE(lowercase_hex(fixture_identity('a'), 64)); - EXPECT_FALSE(lowercase_hex([fixture_identity('a') substringFromIndex:1], 64)); - EXPECT_FALSE(lowercase_hex(fixture_identity('a').uppercaseString, 64)); + EXPECT_FALSE(is_nonempty_string(embedded_nul)); + EXPECT_FALSE(is_relative_path(embedded_nul)); + EXPECT_FALSE(is_lowercase_hex(embedded_nul)); + EXPECT_FALSE(is_nonempty_string(@1)); + EXPECT_FALSE(is_lowercase_hex((id) @1, 64)); + EXPECT_FALSE(is_lowercase_hex((id)NSNull.null, 64)); + EXPECT_FALSE(is_relative_path(@"/ab/model.aimodel")); + EXPECT_FALSE(is_relative_path(@"ab/../model.aimodel")); + EXPECT_FALSE(is_relative_path(@"a\\b")); + EXPECT_FALSE(is_relative_path(@"a//b")); + EXPECT_TRUE(is_relative_path(@"ab/model.aimodel")); + EXPECT_TRUE(is_lowercase_hex(@"ab")); + EXPECT_FALSE(is_lowercase_hex(@"")); + EXPECT_FALSE(is_lowercase_hex(@"AB")); + EXPECT_FALSE(is_lowercase_hex(@"ag")); + EXPECT_TRUE(is_lowercase_hex(fixture_identity('a'), 64)); + EXPECT_FALSE( + is_lowercase_hex([fixture_identity('a') substringFromIndex:1], 64)); + EXPECT_FALSE(is_lowercase_hex(fixture_identity('a').uppercaseString, 64)); } TEST(CoreAIManifestTest, RejectsMismatchedAotPlatformAndArchitecture) { @@ -501,19 +502,19 @@ ios[@"platform"] = @"iOS"; auto parsed_ios = parse_manifest(encode(ios)); ASSERT_TRUE(parsed_ios.ok()); - EXPECT_TRUE(select_assets(parsed_ios.get(), @"arch_b", @"iOS").ok()); + EXPECT_TRUE(select_bundle(parsed_ios.get(), @"arch_b", @"iOS").ok()); auto parsed = parse_manifest(encode(aot_manifest_dict())); ASSERT_TRUE(parsed.ok()); EXPECT_EQ( - select_assets(parsed.get(), @"arch_b", @"iOS").error(), + select_bundle(parsed.get(), @"arch_b", @"iOS").error(), Error::DelegateInvalidCompatibility); EXPECT_EQ( - select_assets(parsed.get(), @"arch_b", nil).error(), + select_bundle(parsed.get(), @"arch_b", nil).error(), Error::DelegateInvalidCompatibility); for (id arch in @[ @"missing", @"ARCH_B", @"", NSNull.null ]) { SCOPED_TRACE([arch description].UTF8String); EXPECT_EQ( - select_assets(parsed.get(), arch == NSNull.null ? nil : arch, @"macOS") + select_bundle(parsed.get(), arch == NSNull.null ? nil : arch, @"macOS") .error(), Error::DelegateInvalidCompatibility); } diff --git a/backends/apple/coreai/runtime/test/coreai_source_fixture.h b/backends/apple/coreai/runtime/test/coreai_source_fixture.h index 561466eb208..d0e51c257fb 100644 --- a/backends/apple/coreai/runtime/test/coreai_source_fixture.h +++ b/backends/apple/coreai/runtime/test/coreai_source_fixture.h @@ -11,6 +11,7 @@ #include #include #include +#include "coreai_bookmark_fixture.h" #include "coreai_filesystem_fixture.h" #include "coreai_manifest_fixture.h" #include "coreai_source_bundle.h" @@ -38,6 +39,10 @@ class TestData final : public runtime::NamedDataMap { }; void aot_data(TestData& data, NSString* arch); +runtime::Result source_manifest(bool aot = false); +NSString* source_key(const SelectedBundle& bundle); +NSURL* staging_root(NSURL* cache_root); +NSURL* source_entry(NSURL* cache_root, const SelectedBundle& bundle); class CoreAISourceTest : public ::testing::Test { protected: diff --git a/backends/apple/coreai/runtime/test/coreai_source_fixture.mm b/backends/apple/coreai/runtime/test/coreai_source_fixture.mm index fbed96d3fa4..93f29fb1cf3 100644 --- a/backends/apple/coreai/runtime/test/coreai_source_fixture.mm +++ b/backends/apple/coreai/runtime/test/coreai_source_fixture.mm @@ -9,7 +9,7 @@ #include "coreai_source_fixture.h" #include -#include "coreai_bookmarks.h" +#include "coreai_cache_entry.h" #include "coreai_cache_key.h" namespace executorch::backends::coreai::testing { @@ -74,6 +74,26 @@ void aot_data(TestData& data, NSString* arch) { data.files[data.required_prefix + "nested/weights.bin"] = "weights"; } +Result source_manifest(bool aot) { + return selected_bundle(aot ? aot_manifest_dict() : manifest_dict()); +} + +NSString* source_key(const SelectedBundle& bundle) { + auto key = cache_variant_key(bundle, @"macOS", @"arch_b", @"default"); + EXPECT_TRUE(key.ok()); + return key.ok() ? key.get() : nil; +} + +NSURL* staging_root(NSURL* cache_root) { + return [cache_root URLByAppendingPathComponent:@"staging" isDirectory:YES]; +} + +NSURL* source_entry(NSURL* cache_root, const SelectedBundle& bundle) { + return + [staging_root(cache_root) URLByAppendingPathComponent:source_key(bundle) + isDirectory:YES]; +} + Result CoreAISourceTest::prepare_source_bundle( const SelectedBundle& bundle, const runtime::NamedDataMap* data, @@ -84,7 +104,7 @@ void aot_data(TestData& data, NSString* arch) { ADD_FAILURE() << "Cannot compute source fixture key"; return key.error(); } - auto lock = lock_cache_entry(cache_root, key.get()); + auto lock = acquire_test_cache_entry(cache_root, key.get()); if (!lock.ok()) { ADD_FAILURE() << "Cannot acquire source fixture lock"; return lock.error(); diff --git a/backends/apple/coreai/runtime/test/coreai_source_integrity_test.mm b/backends/apple/coreai/runtime/test/coreai_source_integrity_test.mm new file mode 100644 index 00000000000..33d75a503d3 --- /dev/null +++ b/backends/apple/coreai/runtime/test/coreai_source_integrity_test.mm @@ -0,0 +1,303 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include "coreai_cache_entry.h" +#include "coreai_cache_key.h" +#include "coreai_fault_scope.h" +#include "coreai_source_fixture.h" +namespace executorch::backends::coreai::testing { +using runtime::Error; +using runtime::Result; +TEST_F(CoreAISourceTest, StagingRemainsAnchoredWhenConfiguredSymlinkChanges) { + TestDirectory storage; + ASSERT_NE(storage.url, nil); + NSURL* target = [storage.url URLByAppendingPathComponent:@"target"]; + NSURL* replacement = [storage.url URLByAppendingPathComponent:@"replacement"]; + NSURL* configured = [storage.url URLByAppendingPathComponent:@"configured"]; + ASSERT_EQ(mkdir(target.fileSystemRepresentation, 0777), 0); + ASSERT_EQ(mkdir(replacement.fileSystemRepresentation, 0777), 0); + ASSERT_EQ( + symlink( + target.fileSystemRepresentation, configured.fileSystemRepresentation), + 0); + auto bundle = source_manifest(); + ASSERT_TRUE(bundle.ok()); + auto key = test_cache_key(bundle.get()); + ASSERT_TRUE(key.ok()); + auto directory = test_cache_directory(configured.path); + ASSERT_TRUE(directory.ok()); + NSString* canonical_target = [directory->path copy]; + auto locked = LockedCacheEntry::acquire(directory.get(), key.get()); + ASSERT_TRUE(locked.ok()); + + ASSERT_EQ(unlink(configured.fileSystemRepresentation), 0); + ASSERT_EQ( + symlink( + replacement.fileSystemRepresentation, + configured.fileSystemRepresentation), + 0); + TestData data; + auto prepared = executorch::backends::coreai::prepare_source_bundle( + bundle.get(), &data, *locked.get()); + ASSERT_TRUE(prepared.ok()); + EXPECT_TRUE([prepared.get().path hasPrefix:canonical_target]); + EXPECT_FALSE([prepared.get().path + hasPrefix:replacement.URLByResolvingSymlinksInPath.path]); + EXPECT_TRUE( + [NSFileManager.defaultManager fileExistsAtPath:prepared.get().path]); +} + +TEST_F(CoreAISourceTest, RejectsMalformedStoredBundlesWithoutReplacement) { + for (bool aot : {false, true}) { + SCOPED_TRACE(aot ? "aot" : "source"); + for (NSString* damage in @[ + @"missing-file", + @"extra-file", + @"wrong-size", + @"symlink-file", + @"file-at-key", + @"extra-directory" + ]) { + SCOPED_TRACE(damage.UTF8String); + TestDirectory storage; + TestDirectory target; + ASSERT_NE(storage.url, nil); + ASSERT_NE(target.url, nil); + TestData data; + if (aot) + aot_data(data, @"arch_b"); + auto manifest = source_manifest(aot); + ASSERT_TRUE(manifest.ok()); + auto source = + prepare_source_bundle(manifest.get(), &data, storage.url.path); + ASSERT_TRUE(source.ok()); + NSURL* entry = source.get().URLByDeletingLastPathComponent; + NSURL* graph = [source.get() URLByAppendingPathComponent:@"graph.bin"]; + NSData* bytes = [NSData dataWithContentsOfURL:graph]; + ASSERT_NE(bytes, nil); + NSFileManager* manager = NSFileManager.defaultManager; + if ([damage isEqual:@"missing-file"]) { + ASSERT_TRUE([manager removeItemAtURL:graph error:nil]); + } else if ([damage isEqual:@"extra-file"]) { + ASSERT_TRUE([[NSData data] + writeToURL:[source.get() URLByAppendingPathComponent:@"extra"] + atomically:NO]); + } else if ([damage isEqual:@"wrong-size"]) { + ASSERT_TRUE([[NSData data] writeToURL:graph atomically:NO]); + } else if ([damage isEqual:@"symlink-file"]) { + NSURL* destination = + [target.url URLByAppendingPathComponent:@"graph.bin"]; + ASSERT_TRUE([bytes writeToURL:destination atomically:NO]); + ASSERT_TRUE([manager removeItemAtURL:graph error:nil]); + ASSERT_TRUE([manager createSymbolicLinkAtPath:graph.path + withDestinationPath:destination.path + error:nil]); + } else if ([damage isEqual:@"file-at-key"]) { + ASSERT_TRUE([manager removeItemAtURL:entry error:nil]); + ASSERT_TRUE([bytes writeToURL:entry atomically:NO]); + } else { + ASSERT_TRUE([manager + createDirectoryAtURL: + [source.get() URLByAppendingPathComponent:@"extra"] + withIntermediateDirectories:NO + attributes:nil + error:nil]); + } + NSDictionary* snapshot = nil; + NSDictionary* target_snapshot = nil; + ASSERT_TRUE(asset_tree_snapshot(storage.url, snapshot)); + ASSERT_TRUE(asset_tree_snapshot(target.url, target_snapshot)); + const int attempts = data.attempts; + EXPECT_EQ( + prepare_source_bundle(manifest.get(), &data, storage.url.path) + .error(), + Error::InvalidExternalData); + EXPECT_EQ(data.attempts, attempts); + EXPECT_TRUE(snapshot_matches(storage.url, snapshot)); + EXPECT_TRUE(snapshot_matches(target.url, target_snapshot)); + EXPECT_EQ(staging_directories(storage.url).count, 0u); + } + } +} + +TEST_F(CoreAISourceTest, ExplicitReplacementRepairsMalformedStoredBundles) { + for (NSString* damage in @[ + @"missing-file", + @"extra-file", + @"wrong-size", + @"symlink-file", + @"fifo-file", + @"file-at-key", + @"symlink-at-key", + @"extra-directory" + ]) { + SCOPED_TRACE(damage.UTF8String); + TestDirectory storage; + TestDirectory target; + ASSERT_NE(storage.url, nil); + ASSERT_NE(target.url, nil); + TestData data; + auto bundle = source_manifest(); + ASSERT_TRUE(bundle.ok()); + auto source = prepare_source_bundle(bundle.get(), &data, storage.url.path); + ASSERT_TRUE(source.ok()); + NSURL* entry = source.get().URLByDeletingLastPathComponent; + NSURL* graph = [source.get() URLByAppendingPathComponent:@"graph.bin"]; + NSFileManager* manager = NSFileManager.defaultManager; + if ([damage isEqual:@"file-at-key"] || [damage isEqual:@"symlink-at-key"]) { + ASSERT_TRUE([manager removeItemAtURL:entry error:nil]); + if ([damage isEqual:@"file-at-key"]) { + ASSERT_TRUE([[NSData data] writeToURL:entry atomically:NO]); + } else { + ASSERT_EQ( + symlink( + target.url.fileSystemRepresentation, + entry.fileSystemRepresentation), + 0); + } + } else if ([damage isEqual:@"extra-file"]) { + ASSERT_TRUE([[NSData data] + writeToURL:[source.get() URLByAppendingPathComponent:@"extra"] + atomically:NO]); + } else if ([damage isEqual:@"extra-directory"]) { + ASSERT_TRUE([manager + createDirectoryAtURL:[source.get() + URLByAppendingPathComponent:@"extra"] + withIntermediateDirectories:NO + attributes:nil + error:nil]); + } else { + ASSERT_TRUE([manager removeItemAtURL:graph error:nil]); + if ([damage isEqual:@"wrong-size"]) { + ASSERT_TRUE([[NSData data] writeToURL:graph atomically:NO]); + } else if ([damage isEqual:@"symlink-file"]) { + NSURL* sentinel = [target.url URLByAppendingPathComponent:@"sentinel"]; + ASSERT_TRUE([@"external" writeToURL:sentinel + atomically:NO + encoding:NSUTF8StringEncoding + error:nil]); + ASSERT_EQ( + symlink( + sentinel.fileSystemRepresentation, + graph.fileSystemRepresentation), + 0); + } else if ([damage isEqual:@"fifo-file"]) { + ASSERT_EQ(mkfifo(graph.fileSystemRepresentation, 0600), 0); + } + } + NSDictionary* target_snapshot = nil; + ASSERT_TRUE(asset_tree_snapshot(target.url, target_snapshot)); + auto repaired = prepare_source_bundle( + bundle.get(), &data, storage.url.path, CorruptBundlePolicy::Replace); + ASSERT_TRUE(repaired.ok()); + EXPECT_TRUE([repaired.get() isEqual:source.get()]); + EXPECT_TRUE(([[NSData + dataWithContentsOfURL:[repaired.get() + URLByAppendingPathComponent:@"graph.bin"]] + isEqual:[NSData dataWithBytes:"model data" length:11]])); + EXPECT_EQ(data.attempts, 2); + EXPECT_EQ(data.requests, data.releases); + EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_TRUE(snapshot_matches(target.url, target_snapshot)); + } +} + +TEST_F(CoreAISourceTest, PublishedDirectoriesUseDefaultPermissions) { + TestDirectory storage; + ASSERT_NE(storage.url, nil); + NSURL* reference = [storage.url URLByAppendingPathComponent:@"reference"]; + ASSERT_EQ(mkdir(reference.fileSystemRepresentation, 0777), 0); + struct stat expected; + ASSERT_EQ(stat(reference.fileSystemRepresentation, &expected), 0); + TestData data; + auto manifest = source_manifest(); + ASSERT_TRUE(manifest.ok()); + auto source = prepare_source_bundle(manifest.get(), &data, storage.url.path); + ASSERT_TRUE(source.ok()); + for (NSURL* directory in + @[ source.get().URLByDeletingLastPathComponent, source.get() ]) { + SCOPED_TRACE(directory.path.UTF8String); + struct stat actual; + ASSERT_EQ(stat(directory.fileSystemRepresentation, &actual), 0); + EXPECT_EQ(actual.st_mode & 0777, expected.st_mode & 0777); + } +} + +TEST_F(CoreAISourceTest, PermissionErrorsRemainDistinctFromCorruptEntries) { + if (geteuid() == 0) { + GTEST_SKIP() << "Root can bypass directory read permissions"; + } + TestDirectory storage; + ASSERT_NE(storage.url, nil); + TestData data; + auto manifest = source_manifest(); + ASSERT_TRUE(manifest.ok()); + auto source = prepare_source_bundle(manifest.get(), &data, storage.url.path); + ASSERT_TRUE(source.ok()); + NSDictionary* snapshot = nil; + ASSERT_TRUE(asset_tree_snapshot(storage.url, snapshot)); + for (NSURL* directory in + @[ source.get().URLByDeletingLastPathComponent, source.get() ]) { + SCOPED_TRACE(directory.path.UTF8String); + struct stat original; + ASSERT_EQ(stat(directory.fileSystemRepresentation, &original), 0); + ASSERT_EQ(chmod(directory.fileSystemRepresentation, 0000), 0); + auto denied = + prepare_source_bundle(manifest.get(), &data, storage.url.path); + EXPECT_EQ( + chmod(directory.fileSystemRepresentation, original.st_mode & 07777), 0); + EXPECT_EQ(denied.error(), Error::AccessFailed); + EXPECT_TRUE(snapshot_matches(storage.url, snapshot)); + EXPECT_EQ(data.attempts, 1); + EXPECT_TRUE( + prepare_source_bundle(manifest.get(), nullptr, storage.url.path).ok()); + } +} + +TEST_F(CoreAISourceTest, PreservesNamedDataErrorsWithoutRetrying) { + for (Error error : {Error::AccessFailed, Error::MemoryAllocationFailed}) { + SCOPED_TRACE(static_cast(error)); + TestDirectory storage; + ASSERT_NE(storage.url, nil); + TestData data; + data.fail_at = 1; + data.failure_error = error; + auto manifest = source_manifest(); + ASSERT_TRUE(manifest.ok()); + EXPECT_EQ( + prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), + error); + EXPECT_EQ(data.attempts, 1); + EXPECT_EQ(data.requests, 0); + EXPECT_EQ(data.releases, 0); + EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_FALSE([NSFileManager.defaultManager + fileExistsAtPath:source_entry(storage.url, manifest.get()).path]); + } +} + +TEST_F(CoreAISourceTest, MissingNamedDataIsInvalidExternalData) { + TestDirectory storage; + ASSERT_NE(storage.url, nil); + TestData data; + data.files.clear(); + auto manifest = source_manifest(); + ASSERT_TRUE(manifest.ok()); + EXPECT_EQ( + prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), + Error::InvalidExternalData); + EXPECT_EQ(data.attempts, 1); + EXPECT_EQ(data.requests, 0); + EXPECT_EQ(staging_directories(storage.url).count, 0u); +} + +} // namespace executorch::backends::coreai::testing diff --git a/backends/apple/coreai/runtime/test/coreai_source_publication_test.mm b/backends/apple/coreai/runtime/test/coreai_source_publication_test.mm new file mode 100644 index 00000000000..19623c90558 --- /dev/null +++ b/backends/apple/coreai/runtime/test/coreai_source_publication_test.mm @@ -0,0 +1,295 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include +#include +#include +#include +#include "coreai_cache_entry.h" +#include "coreai_cache_key.h" +#include "coreai_fault_scope.h" +#include "coreai_source_fixture.h" +namespace executorch::backends::coreai::testing { +using runtime::Error; +using runtime::Result; +TEST_F(CoreAISourceTest, RejectsMismatchedBufferSizesAndReleasesPayload) { + for (size_t size : {0u, 10u, 12u}) { + SCOPED_TRACE(size); + TestDirectory storage; + ASSERT_NE(storage.url, nil); + TestData data; + data.files["coreai/ab/model.aimodel/graph.bin"] = std::string(size, 'x'); + auto manifest = source_manifest(); + ASSERT_TRUE(manifest.ok()); + EXPECT_EQ( + prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), + Error::InvalidExternalData); + EXPECT_EQ(data.attempts, 1); + EXPECT_EQ(data.requests, 1); + EXPECT_EQ(data.releases, 1); + EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_FALSE([NSFileManager.defaultManager + fileExistsAtPath:source_entry(storage.url, manifest.get()).path]); + } +} + +TEST_F(CoreAISourceTest, FullyFlushesSourceBeforeAndAfterPublication) { + TestDirectory storage; + ASSERT_NE(storage.url, nil); + TestData data; + aot_data(data, @"arch_b"); + auto manifest = source_manifest(true); + ASSERT_TRUE(manifest.ok()); + NSURL* final = source_entry(storage.url, manifest.get()); + NSURL* root = staging_root(storage.url); + const NSUInteger file_count = manifest->files.count; + __block int synced_files = 0; + __block int full_syncs = 0; + __block int publications = 0; + __block bool ordered = true; + StorageFaultScope fault(FaultPoint::FullSync); + fault.armed = false; + fault.observe = ^(FaultPoint operation) { + if (operation == FaultPoint::SyncFile) + ++synced_files; + if (operation == FaultPoint::FullSync) { + ++full_syncs; + ordered &= synced_files == file_count; + const bool published = + [NSFileManager.defaultManager fileExistsAtPath:final.path]; + ordered &= full_syncs == 1 ? !published : published; + ordered &= staging_directories(root).count == (full_syncs == 1 ? 1u : 0u); + } + if (operation == FaultPoint::Rename) { + ++publications; + ordered &= full_syncs == 1; + } + }; + auto source = prepare_source_bundle(manifest.get(), &data, storage.url.path); + ASSERT_TRUE(source.ok()); + EXPECT_TRUE(ordered); + EXPECT_EQ(full_syncs, 2); + EXPECT_EQ(publications, 1); + EXPECT_EQ(data.attempts, file_count); + EXPECT_EQ(data.requests, data.releases); +} + +TEST_F(CoreAISourceTest, PublicationFailuresCleanStagingWithoutAutomaticRetry) { + for (auto operation : + {FaultPoint::CreateStagingDirectory, + FaultPoint::OpenStagingDirectory, + FaultPoint::Write, + FaultPoint::SyncFile, + FaultPoint::SyncDirectory, + FaultPoint::FullSync, + FaultPoint::Rename}) { + SCOPED_TRACE(static_cast(operation)); + TestDirectory storage; + ASSERT_NE(storage.url, nil); + TestData data; + auto manifest = source_manifest(); + ASSERT_TRUE(manifest.ok()); + NSString* key = source_key(manifest.get()); + auto lock = acquire_test_cache_entry(storage.url.path, key); + ASSERT_TRUE(lock.ok()); + NSURL* final = source_entry(storage.url, manifest.get()); + { + StorageFaultScope fault(operation); + EXPECT_EQ( + executorch::backends::coreai::prepare_source_bundle( + manifest.get(), &data, *lock.get()) + .error(), + Error::AccessFailed); + EXPECT_EQ(fault.matches, 1); + EXPECT_EQ(fault.hits, 1); + } + const int attempts = operation == FaultPoint::CreateStagingDirectory || + operation == FaultPoint::OpenStagingDirectory || + operation == FaultPoint::SyncDirectory + ? 0 + : 1; + EXPECT_EQ(data.attempts, attempts); + EXPECT_EQ(data.requests, data.releases); + EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:final.path]); + EXPECT_EQ(staging_directories(storage.url).count, 0u); + auto retried = executorch::backends::coreai::prepare_source_bundle( + manifest.get(), &data, *lock.get()); + ASSERT_TRUE(retried.ok()); + EXPECT_EQ(data.attempts, attempts + 1); + } +} + +TEST_F(CoreAISourceTest, RetriesInterruptedStagingAndPublicationCalls) { + for (auto operation : + {FaultPoint::CreateStagingDirectory, + FaultPoint::OpenStagingDirectory, + FaultPoint::SyncDirectory, + FaultPoint::Rename}) { + SCOPED_TRACE(static_cast(operation)); + TestDirectory storage; + ASSERT_NE(storage.url, nil); + TestData data; + auto manifest = source_manifest(); + ASSERT_TRUE(manifest.ok()); + NSString* key = source_key(manifest.get()); + auto lock = acquire_test_cache_entry(storage.url.path, key); + ASSERT_TRUE(lock.ok()); + StorageFaultScope fault(operation); + fault.error = EINTR; + auto source = executorch::backends::coreai::prepare_source_bundle( + manifest.get(), &data, *lock.get()); + ASSERT_TRUE(source.ok()); + EXPECT_EQ(fault.hits, 1); + EXPECT_GE(fault.matches, 2); + EXPECT_EQ(data.attempts, 1); + EXPECT_EQ(data.requests, data.releases); + EXPECT_EQ(staging_directories(storage.url).count, 0u); + } +} + +TEST_F(CoreAISourceTest, PostRenameFlushFailureLeavesCompleteSourceVisible) { + TestDirectory storage; + ASSERT_NE(storage.url, nil); + TestData data; + auto manifest = source_manifest(); + ASSERT_TRUE(manifest.ok()); + NSURL* final = source_entry(storage.url, manifest.get()); + { + StorageFaultScope fault(FaultPoint::FullSync); + fault.match_index = 2; + EXPECT_EQ( + prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), + Error::AccessFailed); + EXPECT_EQ(fault.matches, 2); + EXPECT_EQ(fault.hits, 1); + } + EXPECT_EQ(data.attempts, 1); + EXPECT_EQ(data.requests, data.releases); + EXPECT_EQ(staging_directories(storage.url).count, 0u); + NSURL* graph = [final URLByAppendingPathComponent:@"model.aimodel/graph.bin"]; + EXPECT_TRUE(([[NSData dataWithContentsOfURL:graph] + isEqual:[NSData dataWithBytes:"model data" length:11]])); + NSDictionary* snapshot = nil; + ASSERT_TRUE(asset_tree_snapshot(storage.url, snapshot)); + { + StorageFaultScope fault(FaultPoint::FullSync); + EXPECT_EQ( + prepare_source_bundle(manifest.get(), nullptr, storage.url.path) + .error(), + Error::AccessFailed); + EXPECT_EQ(fault.hits, 1); + EXPECT_TRUE(snapshot_matches(storage.url, snapshot)); + } + StorageFaultScope flush(FaultPoint::FullSync); + flush.error = EINTR; + auto reused = + prepare_source_bundle(manifest.get(), nullptr, storage.url.path); + ASSERT_TRUE(reused.ok()); + EXPECT_EQ(flush.hits, 1); + EXPECT_EQ(flush.matches, 2); + EXPECT_TRUE(snapshot_matches(storage.url, snapshot)); + EXPECT_EQ(data.attempts, 1); +} + +TEST_F(CoreAISourceTest, IncompleteConcurrentWinnerFailsWithoutReplacement) { + TestDirectory storage; + TestDirectory winner_storage; + ASSERT_NE(storage.url, nil); + ASSERT_NE(winner_storage.url, nil); + TestData data; + TestData winner_data; + auto manifest = source_manifest(); + ASSERT_TRUE(manifest.ok()); + auto winner = prepare_source_bundle( + manifest.get(), &winner_data, winner_storage.url.path); + ASSERT_TRUE(winner.ok()); + ASSERT_TRUE([NSFileManager.defaultManager + removeItemAtURL:[winner.get() URLByAppendingPathComponent:@"graph.bin"] + error:nil]); + NSURL* winner_entry = winner.get().URLByDeletingLastPathComponent; + NSDictionary* snapshot = nil; + ASSERT_TRUE(asset_tree_snapshot(winner_entry, snapshot)); + NSURL* root = staging_root(storage.url); + NSURL* final = source_entry(storage.url, manifest.get()); + __block int publications = 0; + __block bool callback_ok = true; + { + StorageFaultScope fault(FaultPoint::Rename); + fault.armed = false; + fault.observe = ^(FaultPoint operation) { + if (operation != FaultPoint::Rename) + return; + ++publications; + callback_ok &= staging_directories(root).count == 1; + callback_ok &= [NSFileManager.defaultManager moveItemAtURL:winner_entry + toURL:final + error:nil]; + }; + EXPECT_EQ( + prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), + Error::InvalidExternalData); + EXPECT_TRUE(callback_ok); + EXPECT_EQ(publications, 1); + } + EXPECT_EQ(data.attempts, 1); + EXPECT_EQ(data.requests, data.releases); + EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_TRUE(snapshot_matches(final, snapshot)); +} + +TEST_F( + CoreAISourceTest, + ExclusivePublicationValidatesWinnerAndRemovesLosingStaging) { + TestDirectory storage; + TestDirectory winner_storage; + ASSERT_NE(storage.url, nil); + ASSERT_NE(winner_storage.url, nil); + TestData data; + TestData winner_data; + auto manifest = source_manifest(); + ASSERT_TRUE(manifest.ok()); + auto winner = prepare_source_bundle( + manifest.get(), &winner_data, winner_storage.url.path); + ASSERT_TRUE(winner.ok()); + NSURL* winner_entry = winner.get().URLByDeletingLastPathComponent; + NSDictionary* snapshot = nil; + ASSERT_TRUE(asset_tree_snapshot(winner_entry, snapshot)); + NSURL* root = staging_root(storage.url); + NSURL* final = source_entry(storage.url, manifest.get()); + __block int publications = 0; + __block bool callback_ok = true; + { + StorageFaultScope fault(FaultPoint::FullSync); + fault.match_index = 2; + fault.error = EINTR; + fault.observe = ^(FaultPoint operation) { + if (operation != FaultPoint::Rename) + return; + ++publications; + callback_ok &= staging_directories(root).count == 1; + callback_ok &= [NSFileManager.defaultManager moveItemAtURL:winner_entry + toURL:final + error:nil]; + }; + auto result = + prepare_source_bundle(manifest.get(), &data, storage.url.path); + EXPECT_TRUE(callback_ok); + ASSERT_TRUE(result.ok()); + EXPECT_TRUE([result.get() + isEqual:[final URLByAppendingPathComponent:manifest->bundle_name]]); + EXPECT_EQ(publications, 1); + EXPECT_EQ(fault.hits, 1); + EXPECT_EQ(fault.matches, 3); + } + EXPECT_EQ(data.attempts, manifest->files.count); + EXPECT_EQ(data.requests, data.releases); + EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_TRUE(snapshot_matches(final, snapshot)); +} + +} // namespace executorch::backends::coreai::testing diff --git a/backends/apple/coreai/runtime/test/coreai_source_test.mm b/backends/apple/coreai/runtime/test/coreai_source_test.mm index 301dafad56e..90bb568337a 100644 --- a/backends/apple/coreai/runtime/test/coreai_source_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_source_test.mm @@ -9,47 +9,17 @@ #include #include #include -#include "coreai_bookmarks.h" +#include +#include "coreai_cache_entry.h" #include "coreai_cache_key.h" #include "coreai_fault_scope.h" #include "coreai_source_fixture.h" +#include "coreai_test_utils.h" namespace executorch::backends::coreai::testing { using runtime::Error; using runtime::Result; -namespace { -Result selected_bundle( - NSDictionary* dictionary, - NSString* architecture = @"arch_b") { - auto parsed = parse_manifest(encode(dictionary)); - if (!parsed.ok()) { - return parsed.error(); - } - return select_assets(parsed.get(), architecture, @"macOS"); -} - -Result source_manifest(bool aot = false) { - return selected_bundle(aot ? aot_manifest_dict() : manifest_dict()); -} - -NSString* source_key(const SelectedBundle& bundle) { - auto key = cache_variant_key(bundle, @"macOS", @"arch_b", @"default"); - EXPECT_TRUE(key.ok()); - return key.ok() ? key.get() : nil; -} - -NSURL* staging_root(NSURL* cache_root) { - return [cache_root URLByAppendingPathComponent:@"staging" isDirectory:YES]; -} - -NSURL* source_entry(NSURL* cache_root, const SelectedBundle& bundle) { - return - [staging_root(cache_root) URLByAppendingPathComponent:source_key(bundle) - isDirectory:YES]; -} -} // namespace - TEST_F(CoreAISourceTest, UnicodePathsMaterializeAndReuse) { NSString* composed = [NSString stringWithFormat:@"caf%C", 0x00e9]; for (NSString* name in @@ -118,7 +88,7 @@ SCOPED_TRACE(arch.UTF8String); TestData data; aot_data(data, arch); - auto selected = select_assets(parsed.get(), arch, @"macOS"); + auto selected = select_bundle(parsed.get(), arch, @"macOS"); ASSERT_TRUE(selected.ok()); auto first = prepare_source_bundle(selected.get(), &data, storage.url.path); ASSERT_TRUE(first.ok()); @@ -155,7 +125,7 @@ unselected_changed[@"files"] = files; auto changed_parsed = parse_manifest(encode(unselected_changed)); ASSERT_TRUE(changed_parsed.ok()); - auto unchanged = select_assets(changed_parsed.get(), arch, @"macOS"); + auto unchanged = select_bundle(changed_parsed.get(), arch, @"macOS"); ASSERT_TRUE(unchanged.ok()); auto unchanged_source = prepare_source_bundle(unchanged.get(), &data, storage.url.path); @@ -203,9 +173,7 @@ auto& worker = *static_cast(context); @autoreleasepool { worker.started = - dispatch_semaphore_wait( - worker.start, - dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC)) == 0; + dispatch_semaphore_wait(worker.start, test_deadline()) == 0; if (!worker.started) return nullptr; auto assets = worker.fixture->prepare_source_bundle( @@ -431,13 +399,13 @@ auto key = cache_variant_key( bundle.get(), @"macOS", @"arch_b", @"explicit-test-options"); ASSERT_TRUE(key.ok()); - auto lock = lock_cache_entry(directory.url.path, key.get()); + auto lock = acquire_test_cache_entry(directory.url.path, key.get()); ASSERT_TRUE(lock.ok()); auto first = executorch::backends::coreai::prepare_source_bundle( bundle.get(), &data, *lock.get()); ASSERT_TRUE(first.ok()); EXPECT_EQ(data.attempts, 1); - NSString* expected_path = [directory.url.path + NSString* expected_path = [directory.url.URLByResolvingSymlinksInPath.path stringByAppendingPathComponent: [NSString stringWithFormat:@"staging/%@/model.aimodel", key.get()]]; EXPECT_TRUE([first.get().path isEqualToString:expected_path]); @@ -457,523 +425,4 @@ EXPECT_TRUE(snapshot_matches(directory.url, snapshot)); } -TEST_F(CoreAISourceTest, RejectsMalformedStoredBundlesWithoutReplacement) { - for (bool aot : {false, true}) { - SCOPED_TRACE(aot ? "aot" : "source"); - for (NSString* damage in @[ - @"missing-file", - @"extra-file", - @"wrong-size", - @"symlink-file", - @"file-at-key", - @"extra-directory" - ]) { - SCOPED_TRACE(damage.UTF8String); - TestDirectory storage; - TestDirectory target; - ASSERT_NE(storage.url, nil); - ASSERT_NE(target.url, nil); - TestData data; - if (aot) - aot_data(data, @"arch_b"); - auto manifest = source_manifest(aot); - ASSERT_TRUE(manifest.ok()); - auto source = - prepare_source_bundle(manifest.get(), &data, storage.url.path); - ASSERT_TRUE(source.ok()); - NSURL* entry = source.get().URLByDeletingLastPathComponent; - NSURL* graph = [source.get() URLByAppendingPathComponent:@"graph.bin"]; - NSData* bytes = [NSData dataWithContentsOfURL:graph]; - ASSERT_NE(bytes, nil); - NSFileManager* manager = NSFileManager.defaultManager; - if ([damage isEqual:@"missing-file"]) { - ASSERT_TRUE([manager removeItemAtURL:graph error:nil]); - } else if ([damage isEqual:@"extra-file"]) { - ASSERT_TRUE([[NSData data] - writeToURL:[source.get() URLByAppendingPathComponent:@"extra"] - atomically:NO]); - } else if ([damage isEqual:@"wrong-size"]) { - ASSERT_TRUE([[NSData data] writeToURL:graph atomically:NO]); - } else if ([damage isEqual:@"symlink-file"]) { - NSURL* destination = - [target.url URLByAppendingPathComponent:@"graph.bin"]; - ASSERT_TRUE([bytes writeToURL:destination atomically:NO]); - ASSERT_TRUE([manager removeItemAtURL:graph error:nil]); - ASSERT_TRUE([manager createSymbolicLinkAtPath:graph.path - withDestinationPath:destination.path - error:nil]); - } else if ([damage isEqual:@"file-at-key"]) { - ASSERT_TRUE([manager removeItemAtURL:entry error:nil]); - ASSERT_TRUE([bytes writeToURL:entry atomically:NO]); - } else { - ASSERT_TRUE([manager - createDirectoryAtURL: - [source.get() URLByAppendingPathComponent:@"extra"] - withIntermediateDirectories:NO - attributes:nil - error:nil]); - } - NSDictionary* snapshot = nil; - NSDictionary* target_snapshot = nil; - ASSERT_TRUE(asset_tree_snapshot(storage.url, snapshot)); - ASSERT_TRUE(asset_tree_snapshot(target.url, target_snapshot)); - const int attempts = data.attempts; - EXPECT_EQ( - prepare_source_bundle(manifest.get(), &data, storage.url.path) - .error(), - Error::InvalidExternalData); - EXPECT_EQ(data.attempts, attempts); - EXPECT_TRUE(snapshot_matches(storage.url, snapshot)); - EXPECT_TRUE(snapshot_matches(target.url, target_snapshot)); - EXPECT_EQ(staging_directories(storage.url).count, 0u); - } - } -} - -TEST_F(CoreAISourceTest, ExplicitReplacementRepairsMalformedStoredBundles) { - for (NSString* damage in @[ - @"missing-file", - @"extra-file", - @"wrong-size", - @"symlink-file", - @"fifo-file", - @"file-at-key", - @"symlink-at-key", - @"extra-directory" - ]) { - SCOPED_TRACE(damage.UTF8String); - TestDirectory storage; - TestDirectory target; - ASSERT_NE(storage.url, nil); - ASSERT_NE(target.url, nil); - TestData data; - auto bundle = source_manifest(); - ASSERT_TRUE(bundle.ok()); - auto source = prepare_source_bundle(bundle.get(), &data, storage.url.path); - ASSERT_TRUE(source.ok()); - NSURL* entry = source.get().URLByDeletingLastPathComponent; - NSURL* graph = [source.get() URLByAppendingPathComponent:@"graph.bin"]; - NSFileManager* manager = NSFileManager.defaultManager; - if ([damage isEqual:@"file-at-key"] || [damage isEqual:@"symlink-at-key"]) { - ASSERT_TRUE([manager removeItemAtURL:entry error:nil]); - if ([damage isEqual:@"file-at-key"]) { - ASSERT_TRUE([[NSData data] writeToURL:entry atomically:NO]); - } else { - ASSERT_EQ( - symlink( - target.url.fileSystemRepresentation, - entry.fileSystemRepresentation), - 0); - } - } else if ([damage isEqual:@"extra-file"]) { - ASSERT_TRUE([[NSData data] - writeToURL:[source.get() URLByAppendingPathComponent:@"extra"] - atomically:NO]); - } else if ([damage isEqual:@"extra-directory"]) { - ASSERT_TRUE([manager - createDirectoryAtURL:[source.get() - URLByAppendingPathComponent:@"extra"] - withIntermediateDirectories:NO - attributes:nil - error:nil]); - } else { - ASSERT_TRUE([manager removeItemAtURL:graph error:nil]); - if ([damage isEqual:@"wrong-size"]) { - ASSERT_TRUE([[NSData data] writeToURL:graph atomically:NO]); - } else if ([damage isEqual:@"symlink-file"]) { - NSURL* sentinel = [target.url URLByAppendingPathComponent:@"sentinel"]; - ASSERT_TRUE([@"external" writeToURL:sentinel - atomically:NO - encoding:NSUTF8StringEncoding - error:nil]); - ASSERT_EQ( - symlink( - sentinel.fileSystemRepresentation, - graph.fileSystemRepresentation), - 0); - } else if ([damage isEqual:@"fifo-file"]) { - ASSERT_EQ(mkfifo(graph.fileSystemRepresentation, 0600), 0); - } - } - NSDictionary* target_snapshot = nil; - ASSERT_TRUE(asset_tree_snapshot(target.url, target_snapshot)); - auto repaired = prepare_source_bundle( - bundle.get(), &data, storage.url.path, CorruptBundlePolicy::Replace); - ASSERT_TRUE(repaired.ok()); - EXPECT_TRUE([repaired.get() isEqual:source.get()]); - EXPECT_TRUE(([[NSData - dataWithContentsOfURL:[repaired.get() - URLByAppendingPathComponent:@"graph.bin"]] - isEqual:[NSData dataWithBytes:"model data" length:11]])); - EXPECT_EQ(data.attempts, 2); - EXPECT_EQ(data.requests, data.releases); - EXPECT_EQ(staging_directories(storage.url).count, 0u); - EXPECT_TRUE(snapshot_matches(target.url, target_snapshot)); - } -} - -TEST_F(CoreAISourceTest, PublishedDirectoriesUseDefaultPermissions) { - TestDirectory storage; - ASSERT_NE(storage.url, nil); - NSURL* reference = [storage.url URLByAppendingPathComponent:@"reference"]; - ASSERT_EQ(mkdir(reference.fileSystemRepresentation, 0777), 0); - struct stat expected; - ASSERT_EQ(stat(reference.fileSystemRepresentation, &expected), 0); - TestData data; - auto manifest = source_manifest(); - ASSERT_TRUE(manifest.ok()); - auto source = prepare_source_bundle(manifest.get(), &data, storage.url.path); - ASSERT_TRUE(source.ok()); - for (NSURL* directory in - @[ source.get().URLByDeletingLastPathComponent, source.get() ]) { - SCOPED_TRACE(directory.path.UTF8String); - struct stat actual; - ASSERT_EQ(stat(directory.fileSystemRepresentation, &actual), 0); - EXPECT_EQ(actual.st_mode & 0777, expected.st_mode & 0777); - } -} - -TEST_F(CoreAISourceTest, PermissionErrorsRemainDistinctFromCorruptEntries) { - if (geteuid() == 0) { - GTEST_SKIP() << "Root can bypass directory read permissions"; - } - TestDirectory storage; - ASSERT_NE(storage.url, nil); - TestData data; - auto manifest = source_manifest(); - ASSERT_TRUE(manifest.ok()); - auto source = prepare_source_bundle(manifest.get(), &data, storage.url.path); - ASSERT_TRUE(source.ok()); - NSDictionary* snapshot = nil; - ASSERT_TRUE(asset_tree_snapshot(storage.url, snapshot)); - for (NSURL* directory in - @[ source.get().URLByDeletingLastPathComponent, source.get() ]) { - SCOPED_TRACE(directory.path.UTF8String); - struct stat original; - ASSERT_EQ(stat(directory.fileSystemRepresentation, &original), 0); - ASSERT_EQ(chmod(directory.fileSystemRepresentation, 0000), 0); - auto denied = - prepare_source_bundle(manifest.get(), &data, storage.url.path); - EXPECT_EQ( - chmod(directory.fileSystemRepresentation, original.st_mode & 07777), 0); - EXPECT_EQ(denied.error(), Error::AccessFailed); - EXPECT_TRUE(snapshot_matches(storage.url, snapshot)); - EXPECT_EQ(data.attempts, 1); - EXPECT_TRUE( - prepare_source_bundle(manifest.get(), nullptr, storage.url.path).ok()); - } -} - -TEST_F(CoreAISourceTest, PreservesNamedDataErrorsWithoutRetrying) { - for (Error error : {Error::AccessFailed, Error::MemoryAllocationFailed}) { - SCOPED_TRACE(static_cast(error)); - TestDirectory storage; - ASSERT_NE(storage.url, nil); - TestData data; - data.fail_at = 1; - data.failure_error = error; - auto manifest = source_manifest(); - ASSERT_TRUE(manifest.ok()); - EXPECT_EQ( - prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), - error); - EXPECT_EQ(data.attempts, 1); - EXPECT_EQ(data.requests, 0); - EXPECT_EQ(data.releases, 0); - EXPECT_EQ(staging_directories(storage.url).count, 0u); - EXPECT_FALSE([NSFileManager.defaultManager - fileExistsAtPath:source_entry(storage.url, manifest.get()).path]); - } -} - -TEST_F(CoreAISourceTest, MissingNamedDataIsInvalidExternalData) { - TestDirectory storage; - ASSERT_NE(storage.url, nil); - TestData data; - data.files.clear(); - auto manifest = source_manifest(); - ASSERT_TRUE(manifest.ok()); - EXPECT_EQ( - prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), - Error::InvalidExternalData); - EXPECT_EQ(data.attempts, 1); - EXPECT_EQ(data.requests, 0); - EXPECT_EQ(staging_directories(storage.url).count, 0u); -} - -TEST_F(CoreAISourceTest, RejectsMismatchedBufferSizesAndReleasesPayload) { - for (size_t size : {0u, 10u, 12u}) { - SCOPED_TRACE(size); - TestDirectory storage; - ASSERT_NE(storage.url, nil); - TestData data; - data.files["coreai/ab/model.aimodel/graph.bin"] = std::string(size, 'x'); - auto manifest = source_manifest(); - ASSERT_TRUE(manifest.ok()); - EXPECT_EQ( - prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), - Error::InvalidExternalData); - EXPECT_EQ(data.attempts, 1); - EXPECT_EQ(data.requests, 1); - EXPECT_EQ(data.releases, 1); - EXPECT_EQ(staging_directories(storage.url).count, 0u); - EXPECT_FALSE([NSFileManager.defaultManager - fileExistsAtPath:source_entry(storage.url, manifest.get()).path]); - } -} - -TEST_F(CoreAISourceTest, FullyFlushesSourceBeforeAndAfterPublication) { - TestDirectory storage; - ASSERT_NE(storage.url, nil); - TestData data; - aot_data(data, @"arch_b"); - auto manifest = source_manifest(true); - ASSERT_TRUE(manifest.ok()); - NSURL* final = source_entry(storage.url, manifest.get()); - NSURL* root = staging_root(storage.url); - const NSUInteger file_count = manifest->files.count; - __block int synced_files = 0; - __block int full_syncs = 0; - __block int publications = 0; - __block bool ordered = true; - StorageFaultScope fault(FaultPoint::FullSync); - fault.armed = false; - fault.observe = ^(FaultPoint operation) { - if (operation == FaultPoint::SyncFile) - ++synced_files; - if (operation == FaultPoint::FullSync) { - ++full_syncs; - ordered &= synced_files == file_count; - const bool published = - [NSFileManager.defaultManager fileExistsAtPath:final.path]; - ordered &= full_syncs == 1 ? !published : published; - ordered &= staging_directories(root).count == (full_syncs == 1 ? 1u : 0u); - } - if (operation == FaultPoint::Rename) { - ++publications; - ordered &= full_syncs == 1; - } - }; - auto source = prepare_source_bundle(manifest.get(), &data, storage.url.path); - ASSERT_TRUE(source.ok()); - EXPECT_TRUE(ordered); - EXPECT_EQ(full_syncs, 2); - EXPECT_EQ(publications, 1); - EXPECT_EQ(data.attempts, file_count); - EXPECT_EQ(data.requests, data.releases); -} - -TEST_F(CoreAISourceTest, PublicationFailuresCleanStagingWithoutAutomaticRetry) { - for (auto operation : - {FaultPoint::CreateStagingDirectory, - FaultPoint::OpenStagingDirectory, - FaultPoint::Write, - FaultPoint::SyncFile, - FaultPoint::SyncDirectory, - FaultPoint::FullSync, - FaultPoint::Rename}) { - SCOPED_TRACE(static_cast(operation)); - TestDirectory storage; - ASSERT_NE(storage.url, nil); - TestData data; - auto manifest = source_manifest(); - ASSERT_TRUE(manifest.ok()); - NSString* key = source_key(manifest.get()); - auto lock = lock_cache_entry(storage.url.path, key); - ASSERT_TRUE(lock.ok()); - NSURL* final = source_entry(storage.url, manifest.get()); - { - StorageFaultScope fault(operation); - EXPECT_EQ( - executorch::backends::coreai::prepare_source_bundle( - manifest.get(), &data, *lock.get()) - .error(), - Error::AccessFailed); - EXPECT_EQ(fault.matches, 1); - EXPECT_EQ(fault.hits, 1); - } - const int attempts = operation == FaultPoint::CreateStagingDirectory || - operation == FaultPoint::OpenStagingDirectory || - operation == FaultPoint::SyncDirectory - ? 0 - : 1; - EXPECT_EQ(data.attempts, attempts); - EXPECT_EQ(data.requests, data.releases); - EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:final.path]); - EXPECT_EQ(staging_directories(storage.url).count, 0u); - auto retried = executorch::backends::coreai::prepare_source_bundle( - manifest.get(), &data, *lock.get()); - ASSERT_TRUE(retried.ok()); - EXPECT_EQ(data.attempts, attempts + 1); - } -} - -TEST_F(CoreAISourceTest, RetriesInterruptedStagingAndPublicationCalls) { - for (auto operation : - {FaultPoint::CreateStagingDirectory, - FaultPoint::OpenStagingDirectory, - FaultPoint::SyncDirectory, - FaultPoint::Rename}) { - SCOPED_TRACE(static_cast(operation)); - TestDirectory storage; - ASSERT_NE(storage.url, nil); - TestData data; - auto manifest = source_manifest(); - ASSERT_TRUE(manifest.ok()); - NSString* key = source_key(manifest.get()); - auto lock = lock_cache_entry(storage.url.path, key); - ASSERT_TRUE(lock.ok()); - StorageFaultScope fault(operation); - fault.error = EINTR; - auto source = executorch::backends::coreai::prepare_source_bundle( - manifest.get(), &data, *lock.get()); - ASSERT_TRUE(source.ok()); - EXPECT_EQ(fault.hits, 1); - EXPECT_GE(fault.matches, 2); - EXPECT_EQ(data.attempts, 1); - EXPECT_EQ(data.requests, data.releases); - EXPECT_EQ(staging_directories(storage.url).count, 0u); - } -} - -TEST_F(CoreAISourceTest, PostRenameFlushFailureLeavesCompleteSourceVisible) { - TestDirectory storage; - ASSERT_NE(storage.url, nil); - TestData data; - auto manifest = source_manifest(); - ASSERT_TRUE(manifest.ok()); - NSURL* final = source_entry(storage.url, manifest.get()); - { - StorageFaultScope fault(FaultPoint::FullSync); - fault.match_index = 2; - EXPECT_EQ( - prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), - Error::AccessFailed); - EXPECT_EQ(fault.matches, 2); - EXPECT_EQ(fault.hits, 1); - } - EXPECT_EQ(data.attempts, 1); - EXPECT_EQ(data.requests, data.releases); - EXPECT_EQ(staging_directories(storage.url).count, 0u); - NSURL* graph = [final URLByAppendingPathComponent:@"model.aimodel/graph.bin"]; - EXPECT_TRUE(([[NSData dataWithContentsOfURL:graph] - isEqual:[NSData dataWithBytes:"model data" length:11]])); - NSDictionary* snapshot = nil; - ASSERT_TRUE(asset_tree_snapshot(storage.url, snapshot)); - { - StorageFaultScope fault(FaultPoint::FullSync); - EXPECT_EQ( - prepare_source_bundle(manifest.get(), nullptr, storage.url.path) - .error(), - Error::AccessFailed); - EXPECT_EQ(fault.hits, 1); - EXPECT_TRUE(snapshot_matches(storage.url, snapshot)); - } - StorageFaultScope flush(FaultPoint::FullSync); - flush.error = EINTR; - auto reused = - prepare_source_bundle(manifest.get(), nullptr, storage.url.path); - ASSERT_TRUE(reused.ok()); - EXPECT_EQ(flush.hits, 1); - EXPECT_EQ(flush.matches, 2); - EXPECT_TRUE(snapshot_matches(storage.url, snapshot)); - EXPECT_EQ(data.attempts, 1); -} - -TEST_F(CoreAISourceTest, IncompleteConcurrentWinnerFailsWithoutReplacement) { - TestDirectory storage; - TestDirectory winner_storage; - ASSERT_NE(storage.url, nil); - ASSERT_NE(winner_storage.url, nil); - TestData data; - TestData winner_data; - auto manifest = source_manifest(); - ASSERT_TRUE(manifest.ok()); - auto winner = prepare_source_bundle( - manifest.get(), &winner_data, winner_storage.url.path); - ASSERT_TRUE(winner.ok()); - ASSERT_TRUE([NSFileManager.defaultManager - removeItemAtURL:[winner.get() URLByAppendingPathComponent:@"graph.bin"] - error:nil]); - NSURL* winner_entry = winner.get().URLByDeletingLastPathComponent; - NSDictionary* snapshot = nil; - ASSERT_TRUE(asset_tree_snapshot(winner_entry, snapshot)); - NSURL* root = staging_root(storage.url); - NSURL* final = source_entry(storage.url, manifest.get()); - __block int publications = 0; - __block bool callback_ok = true; - { - StorageFaultScope fault(FaultPoint::Rename); - fault.armed = false; - fault.observe = ^(FaultPoint operation) { - if (operation != FaultPoint::Rename) - return; - ++publications; - callback_ok &= staging_directories(root).count == 1; - callback_ok &= [NSFileManager.defaultManager moveItemAtURL:winner_entry - toURL:final - error:nil]; - }; - EXPECT_EQ( - prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), - Error::InvalidExternalData); - EXPECT_TRUE(callback_ok); - EXPECT_EQ(publications, 1); - } - EXPECT_EQ(data.attempts, 1); - EXPECT_EQ(data.requests, data.releases); - EXPECT_EQ(staging_directories(storage.url).count, 0u); - EXPECT_TRUE(snapshot_matches(final, snapshot)); -} - -TEST_F( - CoreAISourceTest, - ExclusivePublicationValidatesWinnerAndRemovesLosingStaging) { - TestDirectory storage; - TestDirectory winner_storage; - ASSERT_NE(storage.url, nil); - ASSERT_NE(winner_storage.url, nil); - TestData data; - TestData winner_data; - auto manifest = source_manifest(); - ASSERT_TRUE(manifest.ok()); - auto winner = prepare_source_bundle( - manifest.get(), &winner_data, winner_storage.url.path); - ASSERT_TRUE(winner.ok()); - NSURL* winner_entry = winner.get().URLByDeletingLastPathComponent; - NSDictionary* snapshot = nil; - ASSERT_TRUE(asset_tree_snapshot(winner_entry, snapshot)); - NSURL* root = staging_root(storage.url); - NSURL* final = source_entry(storage.url, manifest.get()); - __block int publications = 0; - __block bool callback_ok = true; - { - StorageFaultScope fault(FaultPoint::FullSync); - fault.match_index = 2; - fault.error = EINTR; - fault.observe = ^(FaultPoint operation) { - if (operation != FaultPoint::Rename) - return; - ++publications; - callback_ok &= staging_directories(root).count == 1; - callback_ok &= [NSFileManager.defaultManager moveItemAtURL:winner_entry - toURL:final - error:nil]; - }; - auto result = - prepare_source_bundle(manifest.get(), &data, storage.url.path); - EXPECT_TRUE(callback_ok); - ASSERT_TRUE(result.ok()); - EXPECT_TRUE([result.get() - isEqual:[final URLByAppendingPathComponent:manifest->bundle_name]]); - EXPECT_EQ(publications, 1); - EXPECT_EQ(fault.hits, 1); - EXPECT_EQ(fault.matches, 3); - } - EXPECT_EQ(data.attempts, manifest->files.count); - EXPECT_EQ(data.requests, data.releases); - EXPECT_EQ(staging_directories(storage.url).count, 0u); - EXPECT_TRUE(snapshot_matches(final, snapshot)); -} - } // namespace executorch::backends::coreai::testing diff --git a/backends/apple/coreai/runtime/test/coreai_storage_test.mm b/backends/apple/coreai/runtime/test/coreai_storage_test.mm index c169197fc01..e5f135724f0 100644 --- a/backends/apple/coreai/runtime/test/coreai_storage_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_storage_test.mm @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "coreai_fault_scope.h" #include "coreai_file.h" @@ -35,12 +36,38 @@ FileDescriptor root( ASSERT_EQ(lstat_at(root.get(), "directory", &info), 0); EXPECT_TRUE(S_ISDIR(info.st_mode)); EXPECT_EQ(open_directory_at(root.get(), "link"), -1); + EXPECT_EQ( + open_storage_directory( + root.get(), @"link", StorageCreationMode::OpenExisting) + .error(), + Error::AccessFailed); EXPECT_EQ(open_directory_at(root.get(), "missing"), -1); EXPECT_EQ(errno, ENOENT); EXPECT_EQ(lstat_at(root.get(), "missing", &info), -1); EXPECT_EQ(errno, ENOENT); } +TEST(CoreAIStorageTest, FileDescriptorMovesTransferSingleOwnership) { + FileDescriptor source(open("/dev/null", O_RDONLY | O_CLOEXEC)); + ASSERT_GE(source.get(), 0); + const int source_fd = source.get(); + FileDescriptor moved(std::move(source)); + EXPECT_EQ(source.get(), -1); + EXPECT_EQ(moved.get(), source_fd); + + FileDescriptor destination(open("/dev/null", O_RDONLY | O_CLOEXEC)); + ASSERT_GE(destination.get(), 0); + const int replaced_fd = destination.get(); + destination = std::move(moved); + EXPECT_EQ(moved.get(), -1); + EXPECT_EQ(destination.get(), source_fd); + EXPECT_EQ(fcntl(replaced_fd, F_GETFD), -1); + EXPECT_EQ(errno, EBADF); + + destination = std::move(destination); + EXPECT_EQ(destination.get(), source_fd); +} + TEST(CoreAIStorageTest, OpensDirectoriesAndReportsCreation) { TestDirectory storage; ASSERT_NE(storage.url, nil); @@ -51,16 +78,17 @@ FileDescriptor root(open( auto missing = open_storage_directory( root.get(), @"child", StorageCreationMode::OpenExisting); ASSERT_TRUE(missing.ok()); - EXPECT_EQ(missing.get().fd, -1); - EXPECT_FALSE(missing.get().created); + EXPECT_FALSE(missing->descriptor.has_value()); + EXPECT_FALSE(missing->created); StorageFaultScope sync_fault(FaultPoint::SyncDirectory); auto created = open_storage_directory(root.get(), @"child", StorageCreationMode::Create); ASSERT_TRUE(created.ok()); - FileDescriptor first(created.get().fd); + ASSERT_TRUE(created->descriptor.has_value()); + FileDescriptor first = std::move(*created->descriptor); ASSERT_GE(first.get(), 0); - EXPECT_TRUE(created.get().created); + EXPECT_TRUE(created->created); EXPECT_NE(fcntl(first.get(), F_GETFD) & FD_CLOEXEC, 0); EXPECT_EQ(sync_fault.matches, 0); struct stat original; @@ -72,9 +100,10 @@ FileDescriptor root(open( {StorageCreationMode::OpenExisting, StorageCreationMode::Create}) { auto reopened = open_storage_directory(root.get(), @"child", mode); ASSERT_TRUE(reopened.ok()); - FileDescriptor next(reopened.get().fd); + ASSERT_TRUE(reopened->descriptor.has_value()); + FileDescriptor next = std::move(*reopened->descriptor); ASSERT_GE(next.get(), 0); - EXPECT_FALSE(reopened.get().created); + EXPECT_FALSE(reopened->created); struct stat current; ASSERT_EQ(fstat(next.get(), ¤t), 0); EXPECT_EQ(current.st_ino, original.st_ino); @@ -99,9 +128,10 @@ FileDescriptor root(open( auto opened = open_storage_directory(root.get(), @"child", StorageCreationMode::Create); ASSERT_TRUE(opened.ok()); - FileDescriptor directory(opened.get().fd); + ASSERT_TRUE(opened->descriptor.has_value()); + FileDescriptor directory = std::move(*opened->descriptor); EXPECT_GE(directory.get(), 0); - EXPECT_FALSE(opened.get().created); + EXPECT_FALSE(opened->created); } TEST(CoreAIStorageTest, RejectsInvalidDirectoryComponents) { diff --git a/backends/apple/coreai/runtime/test/coreai_test_utils.h b/backends/apple/coreai/runtime/test/coreai_test_utils.h new file mode 100644 index 00000000000..88c4bf35ec0 --- /dev/null +++ b/backends/apple/coreai/runtime/test/coreai_test_utils.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace executorch::backends::coreai::testing { + +inline dispatch_time_t test_deadline(uint64_t seconds = 10) { + return dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC); +} + +} // namespace executorch::backends::coreai::testing diff --git a/backends/apple/coreai/runtime/test/export_bridge_fixture.py b/backends/apple/coreai/runtime/test/export_bridge_fixture.py index 797ddb63b6c..2d92aa8025d 100644 --- a/backends/apple/coreai/runtime/test/export_bridge_fixture.py +++ b/backends/apple/coreai/runtime/test/export_bridge_fixture.py @@ -68,7 +68,7 @@ def _export_model( if output_dir.exists(): shutil.rmtree(output_dir) for relative_path in manifest["files"]: - entry = store.pte_data[f"coreai/{manifest['hash']}/{relative_path}"] + entry = store.pte_data[f"coreai/{manifest['export_hash']}/{relative_path}"] destination = output_dir / relative_path destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(store.buffers[entry.buffer_index]) diff --git a/backends/apple/coreai/runtime/test/test_export_bridge_fixture.py b/backends/apple/coreai/runtime/test/test_export_bridge_fixture.py index c8b90230672..c1f4b34a812 100644 --- a/backends/apple/coreai/runtime/test/test_export_bridge_fixture.py +++ b/backends/apple/coreai/runtime/test/test_export_bridge_fixture.py @@ -24,7 +24,7 @@ def test_export_replaces_stale_fixture_files(tmp_path, dynamic, include_pte): bundle.mkdir(parents=True) (bundle / "obsolete.bin").write_bytes(b"obsolete") (output / "stale.json").write_text("{}") - manifest = {"hash": "ab", "files": {"model.aimodel/graph.bin": 3}} + manifest = {"export_hash": "ab", "files": {"model.aimodel/graph.bin": 3}} manifest_bytes = json.dumps(manifest).encode() delegate = SimpleNamespace( processed_bytes=manifest_bytes, diff --git a/backends/apple/coreai/test/test_preprocess.py b/backends/apple/coreai/test/test_preprocess.py index 6570dac278b..5e5bd4b162f 100644 --- a/backends/apple/coreai/test/test_preprocess.py +++ b/backends/apple/coreai/test/test_preprocess.py @@ -144,7 +144,7 @@ def test_inline_embeds_files_in_nds(self): out = result.data_store_output self.assertIsNotNone(out) for rel in manifest["files"]: - self.assertIn(f"coreai/{manifest['hash']}/{rel}", out.pte_data) + self.assertIn(f"coreai/{manifest['export_hash']}/{rel}", out.pte_data) self.assertEqual(out.external_data, {}) # nothing external for inline def test_inline_keys_keep_the_bundle_directory(self): @@ -188,7 +188,7 @@ def test_sizes_and_digests_cover_every_delivered_file(self, _build): result = CoreAIBackend.preprocess(_edge_program(), specs) manifest = json.loads(result.processed_bytes) out = result.data_store_output - prefix = f"coreai/{manifest['hash']}/" + prefix = f"coreai/{manifest['export_hash']}/" embedded = { key[len(prefix) :]: len(out.buffers[entry.buffer_index]) for key, entry in out.pte_data.items() @@ -324,7 +324,7 @@ def test_archs_map_points_at_embedded_bundles(self, _build): result = CoreAIBackend.preprocess(_edge_program(), [_aot_spec(config)]) manifest = json.loads(result.processed_bytes) for arch, rel in manifest["archs"].items(): - self.assertEqual(rel, f"{manifest['hash']}/model.{arch}.aimodelc") + self.assertEqual(rel, f"{manifest['export_hash']}/model.{arch}.aimodelc") self.assertIn(f"coreai/{rel}/model.mil", result.data_store_output.pte_data) @mock.patch(_MOCK_BUILD, side_effect=_fake_run_coreai_build) @@ -348,7 +348,7 @@ def test_two_delegates_get_separate_asset_keys(self, _build): ) lbms = get_lowered_backend_modules(lowered.exported_program().graph_module) self.assertEqual(len(lbms), 2) - hashes = {json.loads(bytes(lbm.processed_bytes))["hash"] for lbm in lbms} + hashes = {json.loads(bytes(lbm.processed_bytes))["export_hash"] for lbm in lbms} self.assertEqual(len(hashes), 2) From 5320ce4e3a188ec013ad3758a5ee949b88e779aa Mon Sep 17 00:00:00 2001 From: Scott Roy Date: Thu, 29 Oct 2026 17:52:09 -0700 Subject: [PATCH 7/7] [CoreAI] Separate cache source and pending directories Rename the per-key source bundle directory to sources/ and in-progress builds to .pending-, so "staging" no longer names two different things. Split cache directory resolution into create and open entry points, which removes an unreachable check in the backend and documents the mode-dependent not-a-directory errors. Share the storage recheck-and-unlink and full-sync steps, return owned descriptors, and drop redundant identity and lock-stripe checks. Rename bookmark test files to cache entry and remove trivial test wrappers. Test Plan: Static CoreAI build of all targets succeeded; all 5 CoreAI CTest targets passed (coreai_host_test: 251 passed, 6 Info-log tests skipped as before). lintrunner CLANGFORMAT passed on changed headers. Shared build, Python suite, and CMake lint were not run. --- backends/apple/coreai/CMakeLists.txt | 6 +- backends/apple/coreai/README.md | 32 ++-- .../apple/coreai/runtime/ETCoreAIBridge.h | 13 +- .../apple/coreai/runtime/coreai_backend.mm | 23 ++- backends/apple/coreai/runtime/coreai_cache.h | 14 +- backends/apple/coreai/runtime/coreai_cache.mm | 3 +- .../apple/coreai/runtime/coreai_cache_entry.h | 21 ++- .../coreai/runtime/coreai_cache_entry.mm | 109 ++++++------- .../apple/coreai/runtime/coreai_cache_key.h | 3 + .../apple/coreai/runtime/coreai_cache_key.mm | 26 +--- .../apple/coreai/runtime/coreai_cache_root.h | 16 +- .../apple/coreai/runtime/coreai_cache_root.mm | 145 ++++++++++-------- .../coreai/runtime/coreai_fault_injection.h | 4 +- backends/apple/coreai/runtime/coreai_file.h | 5 - .../coreai/runtime/coreai_load_coordinator.h | 2 +- .../coreai/runtime/coreai_load_coordinator.mm | 38 +++-- .../apple/coreai/runtime/coreai_manifest.mm | 2 +- .../coreai/runtime/coreai_source_bundle.mm | 124 +++++++-------- .../apple/coreai/runtime/coreai_storage.h | 18 ++- .../apple/coreai/runtime/coreai_storage.mm | 135 ++++++++-------- .../runtime/test/coreai_acquisition_fixture.h | 2 +- .../test/coreai_acquisition_fixture.mm | 12 +- .../runtime/test/coreai_acquisition_test.mm | 58 ++++--- ... => coreai_cache_entry_contention_test.mm} | 12 +- ...fixture.h => coreai_cache_entry_fixture.h} | 4 +- ...xture.mm => coreai_cache_entry_fixture.mm} | 33 ++-- ...ark_test.mm => coreai_cache_entry_test.mm} | 137 +++++++++-------- .../runtime/test/coreai_cache_key_test.mm | 14 +- .../runtime/test/coreai_cache_root_test.mm | 59 ++++--- .../coreai/runtime/test/coreai_cache_test.mm | 21 +-- .../runtime/test/coreai_delegate_test.mm | 4 +- .../runtime/test/coreai_eviction_test.mm | 17 +- .../coreai/runtime/test/coreai_fake_loader.mm | 11 +- .../runtime/test/coreai_filesystem_fixture.h | 18 ++- .../runtime/test/coreai_filesystem_fixture.mm | 20 +-- .../coreai/runtime/test/coreai_host_test.mm | 2 +- .../runtime/test/coreai_source_fixture.h | 5 +- .../runtime/test/coreai_source_fixture.mm | 10 +- .../test/coreai_source_integrity_test.mm | 25 +-- .../test/coreai_source_publication_test.mm | 56 +++---- .../coreai/runtime/test/coreai_source_test.mm | 20 +-- 41 files changed, 654 insertions(+), 625 deletions(-) rename backends/apple/coreai/runtime/test/{coreai_bookmark_contention_test.mm => coreai_cache_entry_contention_test.mm} (98%) rename backends/apple/coreai/runtime/test/{coreai_bookmark_fixture.h => coreai_cache_entry_fixture.h} (94%) rename backends/apple/coreai/runtime/test/{coreai_bookmark_fixture.mm => coreai_cache_entry_fixture.mm} (95%) rename backends/apple/coreai/runtime/test/{coreai_bookmark_test.mm => coreai_cache_entry_test.mm} (82%) diff --git a/backends/apple/coreai/CMakeLists.txt b/backends/apple/coreai/CMakeLists.txt index ddf41fdbbdd..33d0d2d1e33 100644 --- a/backends/apple/coreai/CMakeLists.txt +++ b/backends/apple/coreai/CMakeLists.txt @@ -78,12 +78,12 @@ if(EXECUTORCH_BUILD_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Darwin") runtime/test/coreai_storage_test.mm runtime/test/coreai_filesystem_fixture.mm runtime/test/coreai_cache_key_test.mm - runtime/test/coreai_bookmark_test.mm - runtime/test/coreai_bookmark_contention_test.mm + runtime/test/coreai_cache_entry_test.mm + runtime/test/coreai_cache_entry_contention_test.mm runtime/test/coreai_source_test.mm runtime/test/coreai_source_integrity_test.mm runtime/test/coreai_source_publication_test.mm - runtime/test/coreai_bookmark_fixture.mm + runtime/test/coreai_cache_entry_fixture.mm runtime/test/coreai_source_fixture.mm runtime/test/coreai_fake_loader.mm runtime/test/coreai_acquisition_test.mm diff --git a/backends/apple/coreai/README.md b/backends/apple/coreai/README.md index eb179d22485..e76c209991e 100644 --- a/backends/apple/coreai/README.md +++ b/backends/apple/coreai/README.md @@ -112,16 +112,16 @@ function and ordered I/O on that same acquired model. Backend initialization reads the string runtime spec `coreai_cache_dir`. If absent, it sets the directory to `executorch_coreai` beneath user-domain `NSCachesDirectory`. The coordinator and storage helpers always receive that -explicit directory. It contains raw bookmarks, staged bundles and lock files. +explicit directory. It contains raw bookmarks, source bundles and lock files. The cache directory must be an absolute path reserved for this backend. A -configured symlink is resolved before opening the cache directory. Entry and -staging operations retain directory descriptors and do not follow internal +symlink in the final component of the configured path is resolved before +opening the cache directory. Entry and source operations retain directory descriptors and do not follow internal symlinks. The application must still prevent concurrent renaming, replacement, or modification while SDK calls use source URLs. Written files and changed directories are synced with `fsync`. Bookmark publication uses `F_FULLFSYNC` on the temporary file before rename and on the directory afterward. Source-bundle publication -fully flushes the staging directory after syncing its files and subdirectories, +fully flushes the pending build directory after syncing its files and subdirectories, then fully flushes the destination directory after rename. A failure before rename preserves the old contents; a failure of the final flush reports an error even though the new complete contents are already visible. Use a local APFS @@ -140,8 +140,8 @@ When source recovery is needed, existing bundles are checked for the expected file set, sizes, entry types and directory structure without reading asset contents or requesting NDS payloads. Unicode paths are compared in canonical decomposed form; NDS keys and bytes remain unchanged. Reuse fully flushes the -staging directory, including after a previous publication's final flush failed. -Missing bundles are written to a staging directory and published with an +`sources` directory, including after a previous publication's final flush failed. +Missing bundles are written to a `.pending-` directory and published with an exclusive rename; each selected NDS payload is fetched once and checked for size before writing. If another loader publishes first, its bundle is validated and used. Incomplete, size-mismatched or otherwise malformed existing bundles fail @@ -198,13 +198,13 @@ the platform and Core AI device architecture, plus a hash of a versioned specialization identity supplied as plain data by the SDK boundary. Function bindings and delivery location are excluded, and no model bytes are hashed at runtime. Each variant key determines a raw bookmark in `bookmarks`, a bundle -directory in `staging`, and one of 256 shared coordination files in `locks`. +directory in `sources`, and one of 256 shared coordination files in `locks`. There is no PTE-wide bookmark list or serialized lifecycle record. -A healthy bookmark hit does not consult NDS and attempts to clean leftover -staging. On a missing bookmark or confirmed SDK miss, the coordinator prepares +A healthy bookmark hit does not consult NDS and attempts to clean the leftover +source bundle. On a missing bookmark or confirmed SDK miss, the coordinator prepares the source, specializes it and atomically writes the returned bookmark bytes. -After durable publication, it attempts to remove the entire keyed staging +After durable publication, it attempts to remove the entire keyed source directory while holding the key's stripe lock. The SDK cache supports inference and bookmark restoration without that extracted copy. Keep the packaged named data available: a later SDK cache miss requires source @@ -224,7 +224,7 @@ A fixed pool of at most 256 disk locks coordinates loading and eviction across threads and processes. A SHA-256 hash of the full validated cache key selects a stripe modulo the configured pool size. Files are created on demand, so new model versions do not grow the pool beyond 256. Different keys on the same -stripe serialize; different stripes remain independent. Bookmarks and staging +stripe serialize; different stripes remain independent. Bookmarks and source bundles remain separate for every key. Runtime operations hold only one stripe at a time. @@ -281,16 +281,16 @@ available loader. These synchronous calls require callers to unload affected models and prevent concurrent loads, inference and maintenance across processes. Stripe locks are not a whole-root barrier. Clearing first deletes the recorded SDK entry, then -staging, then its bookmark. Busy or unknown SDK errors preserve the files; +its source bundle, then its bookmark. Busy or unknown SDK errors preserve the files; SDK-confirmed absence permits cleanup. Filesystem errors may leave partial -cleanup, but the bookmark is retained until staging removal succeeds so cleanup +cleanup, but the bookmark is retained until source removal succeeds so cleanup can be retried. Independent keys are still attempted in sorted order, and the first key error is returned. Each removal syncs the directory it changed. A missing root or absent entry is a noncreating no-op. -Everything under a keyed `staging/` directory belongs to the backend and is +Everything under a keyed `sources/` directory belongs to the backend and is removed with that key. Outside keyed entries, clearing does not delete PTEs, -other files or the stable lock files. Unkeyed interrupted-staging directories +other files or the stable lock files. Unkeyed interrupted `.pending-*` directories also remain untouched. Bookmarks are SDK eviction handles, not weights. For an existing entry with a missing or structurally invalid bookmark, clearing logs an Info message with the cache key, attempts local cleanup, and continues to other keys. @@ -309,7 +309,7 @@ Busy errors and all unclassified eviction/restore errors retain the local bookmark for retry; only successful eviction or a confirmed SDK miss permits normal removal. If retries keep failing, first stop every process using the cache directory. Back up or quarantine only the affected key's bookmark and keyed -staging outside that directory, leaving the stable lock files in place. A later load +source bundle outside that directory, leaving the stable lock files in place. A later load can attempt cold acquisition and publish a new bookmark. This manual recovery does not confirm SDK eviction or guarantee disk-space reclamation: keep the backup because it may contain the only remaining eviction diff --git a/backends/apple/coreai/runtime/ETCoreAIBridge.h b/backends/apple/coreai/runtime/ETCoreAIBridge.h index c16f2cae5a9..900aa000aa2 100644 --- a/backends/apple/coreai/runtime/ETCoreAIBridge.h +++ b/backends/apple/coreai/runtime/ETCoreAIBridge.h @@ -26,8 +26,8 @@ typedef NS_ENUM(NSInteger, ETCoreAIErrorCode) { ETCoreAIErrorUnsupported = 2, // Invalid caller arguments or input tensor payloads. ETCoreAIErrorInvalidArgument = 3, - // SDK/system failures or inconsistent SDK outputs, not evidence of - // corruption. + // SDK or system failures, or inconsistent SDK outputs. Does not mean cached + // files are corrupt. ETCoreAIErrorRuntime = 4, }; @@ -49,8 +49,9 @@ FOUNDATION_EXPORT NSErrorDomain const ETCoreAIErrorDomain; NS_DESIGNATED_INITIALIZER; @end -// The copier retains its source storage; SDK pointers stay within their access -// scope. Validate the source layout before constructing. Copying cannot fail. +// Holds the SDK output until copyBytesTo: runs; raw SDK pointers are used only +// inside that call. Construct only after validating the source layout, because +// copying cannot report errors. @interface ETCoreAITensor : NSObject @property(nonatomic, readonly) NSUInteger byteCount; @property(nonatomic, readonly, copy) NSArray* shape; @@ -83,8 +84,8 @@ FOUNDATION_EXPORT NSErrorDomain const ETCoreAIErrorDomain; NSError* _Nullable error))completion; @end -// A prepared model pins the SDK entry. Each session retains its own pin after -// binding. +// Holds the SDK model, keeping its cache entry in use. Bound sessions hold the +// model too, so they stay valid after the prepared model is released. @protocol ETCoreAIPreparedModel - (NSData*)copyBookmarkData; // Completes once with either a session or an error, never both. Completion may diff --git a/backends/apple/coreai/runtime/coreai_backend.mm b/backends/apple/coreai/runtime/coreai_backend.mm index 9d9195bb9e6..7b52edbd767 100644 --- a/backends/apple/coreai/runtime/coreai_backend.mm +++ b/backends/apple/coreai/runtime/coreai_backend.mm @@ -89,9 +89,9 @@ bool is_available() const override { if (!manifest.ok()) { return manifest.error(); } - auto assets = select_host_bundle(manifest.get()); - if (!assets.ok()) { - return assets.error(); + auto host_bundle = select_host_bundle(manifest.get()); + if (!host_bundle.ok()) { + return host_bundle.error(); } auto configured = cache_directory_option(context); if (!configured.ok()) { @@ -100,24 +100,19 @@ bool is_available() const override { id loader = ETCoreAICreateModelLoader(); ET_CHECK_OR_RETURN_ERROR( loader != nil, Internal, "Cannot create Core AI loader"); - auto cache_directory = resolve_cache_directory( - configured.get(), StorageCreationMode::Create); + auto cache_directory = create_cache_directory(configured.get()); if (!cache_directory.ok()) { return cache_directory.error(); } - ET_CHECK_OR_RETURN_ERROR( - cache_directory->has_value(), - AccessFailed, - "Core AI cache directory disappeared"); auto prepared = acquire_model( - assets.get(), + host_bundle.get(), context.get_named_data_map(), - cache_directory->value(), + cache_directory.get(), loader); if (!prepared.ok()) { return prepared.error(); } - auto session = bind_session(prepared.get(), assets->bundle); + auto session = bind_session(prepared.get(), host_bundle->bundle); if (!session.ok()) { return session.error(); } @@ -126,8 +121,8 @@ bool is_available() const override { handle != nullptr, MemoryAllocationFailed, "Cannot allocate Core AI handle"); - handle->input_count = assets->bundle.inputs.count; - handle->output_count = assets->bundle.outputs.count; + handle->input_count = host_bundle->bundle.inputs.count; + handle->output_count = host_bundle->bundle.outputs.count; handle->session = session.get(); processed->Free(); return static_cast(handle.release()); diff --git a/backends/apple/coreai/runtime/coreai_cache.h b/backends/apple/coreai/runtime/coreai_cache.h index f542910d31d..0d2ea862772 100644 --- a/backends/apple/coreai/runtime/coreai_cache.h +++ b/backends/apple/coreai/runtime/coreai_cache.h @@ -16,9 +16,9 @@ class DataLoader; namespace executorch::backends::coreai { /** - * Clears tracked SDK entries, keyed staging, and bookmarks in a dedicated - * Core AI cache directory. Null selects the same default as loading; supplied - * paths must be absolute. Missing directories are successful no-ops. + * Clears tracked SDK entries, per-key source bundles, and bookmarks in a + * dedicated Core AI cache directory. Null selects the same default as loading; + * supplied paths must be absolute. Missing directories are successful no-ops. * PTEs, stable lock files, and unkeyed temporaries are not removed. * SDK entries without saved bookmarks cannot be targeted. * @@ -33,10 +33,10 @@ namespace executorch::backends::coreai { /** * Clears all recorded cache variants referenced by every CoreAI delegate in - * the PTE after validating its manifests. Model prefixes select every device - * and specialization variant. Discovery does not require host compatibility or - * SDK availability. Actual SDK eviction requires a loader. Does not initialize - * delegates or materialize Core AI assets. + * the PTE after validating its manifests. Each exported bundle's identity + * selects every device and specialization variant. Discovery does not require + * host compatibility or SDK availability; evicting an entry with a saved + * bookmark does. Does not initialize delegates or materialize Core AI assets. * * A null directory selects the same Caches default as backend initialization. * Supplied paths must resolve to valid explicit cache directories; they never diff --git a/backends/apple/coreai/runtime/coreai_cache.mm b/backends/apple/coreai/runtime/coreai_cache.mm index 7f20cd861c1..2f6c092eb81 100644 --- a/backends/apple/coreai/runtime/coreai_cache.mm +++ b/backends/apple/coreai/runtime/coreai_cache.mm @@ -27,8 +27,7 @@ if (!configured.ok()) { return configured.error(); } - return resolve_cache_directory( - configured.get(), StorageCreationMode::OpenExisting); + return open_cache_directory(configured.get()); } Error clear_keys(const CacheDirectory& directory, NSArray* keys) { diff --git a/backends/apple/coreai/runtime/coreai_cache_entry.h b/backends/apple/coreai/runtime/coreai_cache_entry.h index 7ff03361247..f3b8230f5b1 100644 --- a/backends/apple/coreai/runtime/coreai_cache_entry.h +++ b/backends/apple/coreai/runtime/coreai_cache_entry.h @@ -32,10 +32,11 @@ struct CacheBookmarkInspection final { enum class CacheEntryPersistence { BookmarkCapable, - StagingOnly, + // Only from acquire_existing when bookmarks/ is missing; never creates it. + SourceOnly, }; -struct CacheStagingDirectory final { +struct CacheSourceDirectory final { FileDescriptor descriptor; NSURL* url; }; @@ -66,9 +67,10 @@ class LockedCacheEntry final { // closed. runtime::Result read_bookmark() const; runtime::Error write_bookmark(NSData* data) const; - runtime::Result prepare_staging_directory() const; - // Missing staging is a no-op; unexpected entry types fail before deletion. - runtime::Error remove_staging() const; + runtime::Result prepare_source_directory() const; + // A missing source bundle is a no-op; unexpected entry types fail before + // deletion. + runtime::Error remove_source() const; // The view must not outlive this locked entry. CacheEntryMaintenance maintenance() const; @@ -87,8 +89,13 @@ class LockedCacheEntry final { const CacheDirectory& directory, NSString* key, StorageCreationMode mode); - runtime::Result> open_staging_directory( + runtime::Result> open_source_directory( StorageCreationMode mode) const; + // Applies a storage operation to this key's source bundle. A missing + // sources/ directory is a no-op. + runtime::Error apply_to_source( + runtime::Error (*operation)(int, NSString*, StorageRemovalPolicy), + StorageRemovalPolicy policy) const; FileDescriptor root_; FileDescriptor bookmarks_; @@ -102,7 +109,7 @@ class CacheEntryMaintenance final { public: runtime::Result inspect_bookmark() const; runtime::Error preflight() const; - runtime::Error remove_staging() const; + runtime::Error remove_source() const; runtime::Error remove_bookmark() const; private: diff --git a/backends/apple/coreai/runtime/coreai_cache_entry.mm b/backends/apple/coreai/runtime/coreai_cache_entry.mm index d8fa58900e0..ce363e04760 100644 --- a/backends/apple/coreai/runtime/coreai_cache_entry.mm +++ b/backends/apple/coreai/runtime/coreai_cache_entry.mm @@ -26,12 +26,12 @@ constexpr size_t kBookmarkByteLimit = 8 * 1024 * 1024; NSString* const kBookmarksDirectory = @"bookmarks"; -NSString* const kStagingDirectory = @"staging"; +NSString* const kSourcesDirectory = @"sources"; NSString* const kLocksDirectory = @"locks"; -NSString* const kBookmarkExtension = @".bookmark"; +NSString* const kBookmarkSuffix = @(kBookmarkExtension); NSString* bookmark_name(NSString* key) { - return [key stringByAppendingString:kBookmarkExtension]; + return [key stringByAppendingString:kBookmarkSuffix]; } Result> @@ -48,7 +48,7 @@ Result entry_exists(int root_fd, NSString* key) { for (NSString* directory_name in - @[ kBookmarksDirectory, kStagingDirectory ]) { + @[ kBookmarksDirectory, kSourcesDirectory ]) { auto opened = open_child(root_fd, directory_name, StorageCreationMode::OpenExisting); if (!opened.ok()) { @@ -86,7 +86,7 @@ FileDescriptor duplicate( Result*> inventory_cache_keys( const CacheDirectory& directory) { NSMutableSet* keys = [NSMutableSet set]; - for (NSString* child_name in @[ kBookmarksDirectory, kStagingDirectory ]) { + for (NSString* child_name in @[ kBookmarksDirectory, kSourcesDirectory ]) { auto child = open_child( directory.descriptor.get(), child_name, @@ -105,11 +105,11 @@ FileDescriptor duplicate( for (NSString* name in names.get()) { NSString* key = name; if ([child_name isEqual:kBookmarksDirectory]) { - if (![name hasSuffix:kBookmarkExtension] || - name.length <= kBookmarkExtension.length) { + if (![name hasSuffix:kBookmarkSuffix] || + name.length <= kBookmarkSuffix.length) { continue; } - key = [name substringToIndex:name.length - kBookmarkExtension.length]; + key = [name substringToIndex:name.length - kBookmarkSuffix.length]; } if (is_cache_key(key)) { [keys addObject:key]; @@ -159,6 +159,7 @@ FileDescriptor duplicate( } FileDescriptor directory = std::move(duplicated.get()); const bool create = mode == StorageCreationMode::Create; + // Checking before locking avoids creating locks/ for a missing entry. if (!create) { auto exists = entry_exists(directory.get(), key); if (!exists.ok()) { @@ -189,6 +190,7 @@ FileDescriptor lock_file(open_storage_shared_file( retry_eintr([&] { return flock(lock_file.get(), LOCK_EX); }) == 0, AccessFailed, "Cannot lock Core AI cache entry"); + // Another process may have cleared the entry while this one waited. if (!create) { auto exists = entry_exists(directory.get(), key); if (!exists.ok()) { @@ -204,7 +206,7 @@ FileDescriptor lock_file(open_storage_shared_file( } const CacheEntryPersistence persistence = bookmarks->has_value() ? CacheEntryPersistence::BookmarkCapable - : CacheEntryPersistence::StagingOnly; + : CacheEntryPersistence::SourceOnly; FileDescriptor bookmark_directory = bookmarks->has_value() ? std::move(bookmarks->value()) : FileDescriptor(); return std::unique_ptr(new LockedCacheEntry( @@ -217,7 +219,7 @@ FileDescriptor lock_file(open_storage_shared_file( } Result LockedCacheEntry::read_bookmark() const { - if (persistence_ == CacheEntryPersistence::StagingOnly) { + if (persistence_ == CacheEntryPersistence::SourceOnly) { return static_cast(nil); } FileDescriptor file(retry_eintr([&] { @@ -243,13 +245,12 @@ FileDescriptor file(retry_eintr([&] { bytes != nil, MemoryAllocationFailed, "Cannot allocate Core AI bookmark"); size_t offset = 0; while (offset < bytes.length) { - const ssize_t count = ::read( - file.get(), - static_cast(bytes.mutableBytes) + offset, - bytes.length - offset); - if (count < 0 && errno == EINTR) { - continue; - } + const ssize_t count = retry_eintr([&] { + return ::read( + file.get(), + static_cast(bytes.mutableBytes) + offset, + bytes.length - offset); + }); ET_CHECK_OR_RETURN_ERROR( count > 0, AccessFailed, "Cannot read Core AI bookmark"); offset += count; @@ -266,37 +267,43 @@ FileDescriptor file(retry_eintr([&] { ET_CHECK_OR_RETURN_ERROR( persistence_ == CacheEntryPersistence::BookmarkCapable, InvalidArgument, - "Cannot write through a staging-only Core AI cache entry lock"); + "Cannot write through a source-only Core AI cache entry lock"); return publish_storage_data(bookmarks_.get(), bookmark_name(key_), data); } -Result> LockedCacheEntry::open_staging_directory( +Result> LockedCacheEntry::open_source_directory( StorageCreationMode mode) const { - return open_child(root_.get(), kStagingDirectory, mode); + return open_child(root_.get(), kSourcesDirectory, mode); } -Result LockedCacheEntry::prepare_staging_directory() +Error LockedCacheEntry::apply_to_source( + Error (*operation)(int, NSString*, StorageRemovalPolicy), + StorageRemovalPolicy policy) const { + auto sources = open_source_directory(StorageCreationMode::OpenExisting); + if (!sources.ok()) { + return sources.error(); + } + if (!sources->has_value()) { + return Error::Ok; + } + return operation(sources->value().get(), key_, policy); +} + +Result LockedCacheEntry::prepare_source_directory() const { - auto staging = open_staging_directory(StorageCreationMode::Create); - if (!staging.ok()) { - return staging.error(); + auto sources = open_source_directory(StorageCreationMode::Create); + if (!sources.ok()) { + return sources.error(); } NSURL* url = [[NSURL fileURLWithPath:root_path_ isDirectory:YES] - URLByAppendingPathComponent:kStagingDirectory + URLByAppendingPathComponent:kSourcesDirectory isDirectory:YES]; - return CacheStagingDirectory{std::move(staging->value()), url}; + return CacheSourceDirectory{std::move(sources->value()), url}; } -Error LockedCacheEntry::remove_staging() const { - auto staging = open_staging_directory(StorageCreationMode::OpenExisting); - if (!staging.ok()) { - return staging.error(); - } - if (!staging->has_value()) { - return Error::Ok; - } - return remove_storage_directory( - staging->value().get(), key_, StorageRemovalPolicy::Strict); +Error LockedCacheEntry::remove_source() const { + return apply_to_source( + remove_storage_directory, StorageRemovalPolicy::Strict); } CacheEntryMaintenance LockedCacheEntry::maintenance() const { @@ -305,7 +312,7 @@ FileDescriptor file(retry_eintr([&] { Result CacheEntryMaintenance::inspect_bookmark() const { - if (entry_.persistence_ == CacheEntryPersistence::StagingOnly) { + if (entry_.persistence_ == CacheEntryPersistence::SourceOnly) { return CacheBookmarkInspection{CacheBookmarkState::Missing, nil}; } struct stat info; @@ -341,33 +348,17 @@ FileDescriptor file(retry_eintr([&] { bookmark_name(entry_.key_), StorageRemovalPolicy::Maintenance)); } - auto staging = - entry_.open_staging_directory(StorageCreationMode::OpenExisting); - if (!staging.ok()) { - return staging.error(); - } - if (!staging->has_value()) { - return Error::Ok; - } - return preflight_storage_directory( - staging->value().get(), entry_.key_, StorageRemovalPolicy::Maintenance); + return entry_.apply_to_source( + preflight_storage_directory, StorageRemovalPolicy::Maintenance); } -Error CacheEntryMaintenance::remove_staging() const { - auto staging = - entry_.open_staging_directory(StorageCreationMode::OpenExisting); - if (!staging.ok()) { - return staging.error(); - } - if (!staging->has_value()) { - return Error::Ok; - } - return remove_storage_directory( - staging->value().get(), entry_.key_, StorageRemovalPolicy::Maintenance); +Error CacheEntryMaintenance::remove_source() const { + return entry_.apply_to_source( + remove_storage_directory, StorageRemovalPolicy::Maintenance); } Error CacheEntryMaintenance::remove_bookmark() const { - if (entry_.persistence_ == CacheEntryPersistence::StagingOnly) { + if (entry_.persistence_ == CacheEntryPersistence::SourceOnly) { return Error::Ok; } return remove_storage_directory( diff --git a/backends/apple/coreai/runtime/coreai_cache_key.h b/backends/apple/coreai/runtime/coreai_cache_key.h index 3f91fa04640..b252821cbb3 100644 --- a/backends/apple/coreai/runtime/coreai_cache_key.h +++ b/backends/apple/coreai/runtime/coreai_cache_key.h @@ -19,6 +19,9 @@ class SelectedBundle; bool is_cache_key(NSString* key); NSString* cache_key_bundle_identity(NSString* key); +// Bookmark files are named key + extension, which bounds the key length. +inline constexpr char kBookmarkExtension[] = ".bookmark"; + // Bundle identity excludes device and specialization so maintenance can find // every runtime variant of one exported bundle. runtime::Result bundle_identity( diff --git a/backends/apple/coreai/runtime/coreai_cache_key.mm b/backends/apple/coreai/runtime/coreai_cache_key.mm index 7dafbf83c69..8f63ce7fd25 100644 --- a/backends/apple/coreai/runtime/coreai_cache_key.mm +++ b/backends/apple/coreai/runtime/coreai_cache_key.mm @@ -18,8 +18,8 @@ constexpr NSUInteger kHashLength = 2 * CC_SHA256_DIGEST_LENGTH; constexpr NSUInteger kFixedLength = 2 * (kHashLength + 1); -constexpr NSUInteger kBookmarkExtensionLength = sizeof(".bookmark") - 1; -constexpr NSUInteger kMaxKeyLength = NAME_MAX - kBookmarkExtensionLength; +constexpr NSUInteger kMaxKeyLength = + NAME_MAX - (sizeof(kBookmarkExtension) - 1); static_assert(kCacheEntryLockStripeCount > 0); static_assert(kCacheEntryLockStripeCount <= 256); @@ -78,11 +78,7 @@ bool valid_device(NSString* device) { NSString* digest, NSString* bundle_name, bool aot_compiled) { - ET_CHECK_OR_RETURN_ERROR( - digest != nil && is_relative_path(bundle_name) && - ![bundle_name containsString:@"/"], - DelegateInvalidCompatibility, - "Invalid Core AI bundle identity"); + // Bumping the leading version makes every existing cache entry unreachable. auto encoded = encode_json( @[ @"1", digest, aot_compiled ? @"aot" : @"source", bundle_name ]); if (!encoded.ok()) { @@ -178,17 +174,11 @@ bool is_cache_key(NSString* key) { ET_CHECK_OR_RETURN_ERROR( is_cache_key(key), InvalidArgument, "Invalid Core AI cache key"); NSData* encoded = [key dataUsingEncoding:NSUTF8StringEncoding]; - auto hash = hash_data(encoded); - if (!hash.ok()) { - return hash.error(); - } - unsigned int first_byte = 0; - const bool parsed = - [[NSScanner scannerWithString:[hash.get() substringToIndex:2]] - scanHexInt:&first_byte]; - ET_CHECK_OR_RETURN_ERROR( - parsed, InvalidExternalData, "Cannot derive Core AI cache lock stripe"); - const size_t stripe = first_byte % kCacheEntryLockStripeCount; + unsigned char digest[CC_SHA256_DIGEST_LENGTH]; + CC_SHA256(encoded.bytes, static_cast(encoded.length), digest); + const size_t stripe = digest[0] % kCacheEntryLockStripeCount; + // A different stripe scheme needs a new prefix so its files never alias + // these. return [NSString stringWithFormat:@"stripe-v1-%02zx.lock", stripe]; } diff --git a/backends/apple/coreai/runtime/coreai_cache_root.h b/backends/apple/coreai/runtime/coreai_cache_root.h index 143c6b86132..f2c3f696202 100644 --- a/backends/apple/coreai/runtime/coreai_cache_root.h +++ b/backends/apple/coreai/runtime/coreai_cache_root.h @@ -24,10 +24,16 @@ struct CacheDirectory final { runtime::Result default_cache_directory(); // Null selects the default; invalid explicit paths never fall back to it. runtime::Result cache_directory_path(const char* configured_path); -// Resolves symlinks once and anchors the resulting directory with an owned -// descriptor. OpenExisting returns no value when the directory is missing. -runtime::Result> resolve_cache_directory( - NSString* path, - StorageCreationMode mode); + +// Both functions resolve a symlink in the final path component once and hold +// the directory open, so later path changes cannot redirect the cache. +// +// Creates the directory if missing and excludes it from backup. A path that is +// not a directory is the caller's configuration error: InvalidArgument. +runtime::Result create_cache_directory(NSString* path); +// Returns no value when the directory is missing. A path that is not a +// directory means existing storage is unusable: AccessFailed. +runtime::Result> open_cache_directory( + NSString* path); } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_cache_root.mm b/backends/apple/coreai/runtime/coreai_cache_root.mm index f4b7d4123f1..abc039bed0f 100644 --- a/backends/apple/coreai/runtime/coreai_cache_root.mm +++ b/backends/apple/coreai/runtime/coreai_cache_root.mm @@ -64,32 +64,36 @@ Error ensure_excluded_from_backup(NSString* path) { return canonical; } -Result default_cache_directory_impl() { - NSURL* caches = - [NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory - inDomains:NSUserDomainMask] - .firstObject; - ET_CHECK_OR_RETURN_ERROR( - caches.isFileURL, AccessFailed, "Cannot resolve Core AI cache directory"); - return [caches.path stringByAppendingPathComponent:@"executorch_coreai"]; +// isAbsolutePath accepts "~" paths, which the filesystem would treat literally. +bool is_cache_directory_path(NSString* path) { + return is_nonempty_string(path) && path.isAbsolutePath && + ![path hasPrefix:@"~"]; } -} // namespace - -Result default_cache_directory() { - return default_cache_directory_impl(); +Error create_directory(NSString* path) { + NSDictionary* attributes = @{ + NSFileProtectionKey : NSFileProtectionCompleteUntilFirstUserAuthentication, + }; + ET_CHECK_OR_RETURN_ERROR( + fault_point(FaultPoint::BeforeCreateDirectory) == 0, + AccessFailed, + "Cannot begin Core AI cache directory creation"); + NSError* error = nil; + ET_CHECK_OR_RETURN_ERROR( + [NSFileManager.defaultManager createDirectoryAtPath:path + withIntermediateDirectories:YES + attributes:attributes + error:&error], + AccessFailed, + "Cannot create Core AI cache directory: %s", + error.localizedDescription.UTF8String); + return Error::Ok; } -Result cache_directory_path(const char* configured_path) { - if (configured_path == nullptr) { - return default_cache_directory(); - } - NSString* path = [NSString stringWithUTF8String:configured_path]; - ET_CHECK_OR_RETURN_ERROR( - is_nonempty_string(path) && path.isAbsolutePath && ![path hasPrefix:@"~"], - InvalidArgument, - "Core AI cache directory requires an absolute UTF-8 path"); - return path; +Error not_a_directory(StorageCreationMode mode) { + ET_LOG(Error, "Core AI cache path is not a directory"); + return mode == StorageCreationMode::Create ? Error::InvalidArgument + : Error::AccessFailed; } Result> resolve_cache_directory( @@ -97,56 +101,32 @@ Error ensure_excluded_from_backup(NSString* path) { StorageCreationMode mode) { const bool create = mode == StorageCreationMode::Create; ET_CHECK_OR_RETURN_ERROR( - is_nonempty_string(path) && path.isAbsolutePath && ![path hasPrefix:@"~"], + is_cache_directory_path(path), InvalidArgument, "Core AI cache directory requires an absolute path"); - struct stat link_info; - const int link_inspected = retry_eintr( - [&] { return lstat(path.fileSystemRepresentation, &link_info); }); struct stat info; - int inspected = - retry_eintr([&] { return stat(path.fileSystemRepresentation, &info); }); - if (inspected < 0 && errno == ENOENT && !create) { - return std::optional(); - } - if (inspected < 0 && errno == ENOTDIR) { - if (create) { - return Error::InvalidArgument; - } - return Error::AccessFailed; - } - ET_CHECK_OR_RETURN_ERROR( - inspected == 0 || (create && errno == ENOENT), - AccessFailed, - "Cannot inspect Core AI cache directory"); - if (inspected == 0 && !S_ISDIR(info.st_mode)) { - ET_LOG(Error, "Core AI cache path is not a directory"); - if (create) { - return Error::InvalidArgument; + if (retry_eintr([&] { return stat(path.fileSystemRepresentation, &info); }) != + 0) { + if (errno == ENOTDIR) { + return not_a_directory(mode); } - return Error::AccessFailed; - } - if (inspected < 0) { - NSDictionary* attributes = @{ - NSFileProtectionKey : - NSFileProtectionCompleteUntilFirstUserAuthentication, - }; - ET_CHECK_OR_RETURN_ERROR( - fault_point(FaultPoint::BeforeCreateDirectory) == 0, - AccessFailed, - "Cannot begin Core AI cache directory creation"); - NSError* error = nil; ET_CHECK_OR_RETURN_ERROR( - [NSFileManager.defaultManager createDirectoryAtPath:path - withIntermediateDirectories:YES - attributes:attributes - error:&error], + errno == ENOENT, AccessFailed, - "Cannot create Core AI cache directory: %s", - error.localizedDescription.UTF8String); + "Cannot inspect Core AI cache directory"); + if (!create) { + return std::optional(); + } + ET_CHECK_OK_OR_RETURN_ERROR(create_directory(path)); + } else if (!S_ISDIR(info.st_mode)) { + return not_a_directory(mode); } NSString* resolved_path = path; - if (link_inspected == 0 && S_ISLNK(link_info.st_mode)) { + struct stat link_info; + if (retry_eintr([&] { + return lstat(path.fileSystemRepresentation, &link_info); + }) == 0 && + S_ISLNK(link_info.st_mode)) { auto canonical = canonical_path(path); if (!canonical.ok()) { return canonical.error(); @@ -174,4 +154,41 @@ FileDescriptor directory(retry_eintr([&] { CacheDirectory{[resolved_path copy], std::move(directory)}); } +} // namespace + +Result default_cache_directory() { + NSURL* caches = + [NSFileManager.defaultManager URLsForDirectory:NSCachesDirectory + inDomains:NSUserDomainMask] + .firstObject; + ET_CHECK_OR_RETURN_ERROR( + caches.isFileURL, AccessFailed, "Cannot resolve Core AI cache directory"); + return [caches.path stringByAppendingPathComponent:@"executorch_coreai"]; +} + +Result cache_directory_path(const char* configured_path) { + if (configured_path == nullptr) { + return default_cache_directory(); + } + NSString* path = [NSString stringWithUTF8String:configured_path]; + ET_CHECK_OR_RETURN_ERROR( + is_cache_directory_path(path), + InvalidArgument, + "Core AI cache directory requires an absolute UTF-8 path"); + return path; +} + +Result create_cache_directory(NSString* path) { + auto directory = resolve_cache_directory(path, StorageCreationMode::Create); + if (!directory.ok()) { + return directory.error(); + } + // Create mode reports a missing directory as an error, never as no value. + return std::move(**directory); +} + +Result> open_cache_directory(NSString* path) { + return resolve_cache_directory(path, StorageCreationMode::OpenExisting); +} + } // namespace executorch::backends::coreai diff --git a/backends/apple/coreai/runtime/coreai_fault_injection.h b/backends/apple/coreai/runtime/coreai_fault_injection.h index 275ef14e157..79b9f38da6b 100644 --- a/backends/apple/coreai/runtime/coreai_fault_injection.h +++ b/backends/apple/coreai/runtime/coreai_fault_injection.h @@ -21,8 +21,8 @@ enum class FaultPoint { FullSync, Rename, Remove, - CreateStagingDirectory, - OpenStagingDirectory, + CreatePendingDirectory, + OpenPendingDirectory, BeforeCreateDirectory, BeforeCacheEntryLock, BeforeSDK, diff --git a/backends/apple/coreai/runtime/coreai_file.h b/backends/apple/coreai/runtime/coreai_file.h index 19433fbf2e5..473c8d5bb7d 100644 --- a/backends/apple/coreai/runtime/coreai_file.h +++ b/backends/apple/coreai/runtime/coreai_file.h @@ -15,11 +15,6 @@ namespace executorch::backends::coreai { -enum class StorageCreationMode { - OpenExisting, - Create, -}; - class FileDescriptor { public: explicit FileDescriptor(int fd = -1) : fd_(fd) {} diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.h b/backends/apple/coreai/runtime/coreai_load_coordinator.h index 5bb7655674f..0cff9087e7d 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.h +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.h @@ -26,7 +26,7 @@ runtime::Result select_host_bundle( // The cache entry lock outlives all SDK callbacks and bookmark publication. // Cleanup after a restore hit or durable publication is best-effort. runtime::Result> acquire_model( - const SelectedHostBundle& assets, + const SelectedHostBundle& host_bundle, const runtime::NamedDataMap* named_data, const CacheDirectory& cache_directory, id loader); diff --git a/backends/apple/coreai/runtime/coreai_load_coordinator.mm b/backends/apple/coreai/runtime/coreai_load_coordinator.mm index 6e89c14cf60..bd4f45a7de1 100644 --- a/backends/apple/coreai/runtime/coreai_load_coordinator.mm +++ b/backends/apple/coreai/runtime/coreai_load_coordinator.mm @@ -21,6 +21,14 @@ using runtime::Error; using runtime::Result; +Error require_coreai_available() { + ET_CHECK_OR_RETURN_ERROR( + ETCoreAIIsAvailable(), + NotSupported, + "Core AI requires macOS 27 or iOS 27"); + return Error::Ok; +} + Result> wait_for_acquisition( void (^operation)(ETCoreAIAcquisitionCompletion)) { ET_CHECK_OR_RETURN_ERROR( @@ -71,11 +79,11 @@ } void cleanup_source(const LockedCacheEntry& cache_entry) { - const Error error = cache_entry.remove_staging(); + const Error error = cache_entry.remove_source(); if (error != Error::Ok) { ET_LOG( Error, - "Cannot clean Core AI staging: error=%u", + "Cannot remove Core AI source bundle: error=%u", static_cast(error)); } } @@ -120,10 +128,7 @@ Error evict_one(id loader, NSData* bookmark) { isOperatingSystemAtLeastVersion:manifest.minimum_version], DelegateInvalidCompatibility, "Core AI model requires a newer operating system"); - ET_CHECK_OR_RETURN_ERROR( - ETCoreAIIsAvailable(), - NotSupported, - "Core AI requires macOS 27 or iOS 27"); + ET_CHECK_OK_OR_RETURN_ERROR(require_coreai_available()); #if TARGET_OS_OSX NSString* platform = @"macOS"; #elif TARGET_OS_IOS @@ -141,7 +146,7 @@ Error evict_one(id loader, NSData* bookmark) { } Result> acquire_model( - const SelectedHostBundle& assets, + const SelectedHostBundle& host_bundle, const runtime::NamedDataMap* named_data, const CacheDirectory& cache_directory, id loader) { @@ -149,7 +154,10 @@ Error evict_one(id loader, NSData* bookmark) { loader != nil, InvalidArgument, "Missing Core AI model loader"); NSString* specialization = [loader.specializationIdentifier copy]; auto key = cache_variant_key( - assets.bundle, assets.platform, assets.architecture, specialization); + host_bundle.bundle, + host_bundle.platform, + host_bundle.architecture, + specialization); if (!key.ok()) { return key.error(); } @@ -185,7 +193,10 @@ Error evict_one(id loader, NSData* bookmark) { Info, "Core AI cache miss: no bookmark, key=%s", cache_key.UTF8String); } auto source = prepare_source_bundle( - assets.bundle, named_data, *locked.get(), CorruptBundlePolicy::Replace); + host_bundle.bundle, + named_data, + *locked.get(), + CorruptBundlePolicy::Replace); if (!source.ok()) { return source.error(); } @@ -195,7 +206,7 @@ Error evict_one(id loader, NSData* bookmark) { ET_LOG( Info, "Core AI specialization started: kind=%s, key=%s", - assets.bundle.aot_compiled ? "aot" : "source", + host_bundle.bundle.aot_compiled ? "aot" : "source", cache_key.UTF8String); [loader specializeModelAtURL:url completion:completion]; }); @@ -243,10 +254,7 @@ Error clear_cache_entry( bookmark->state == CacheBookmarkState::Missing ? "missing" : "corrupt"); } else { if (loader == nil) { - ET_CHECK_OR_RETURN_ERROR( - ETCoreAIIsAvailable(), - NotSupported, - "Core AI requires macOS 27 or iOS 27"); + ET_CHECK_OK_OR_RETURN_ERROR(require_coreai_available()); loader = ETCoreAICreateModelLoader(); ET_CHECK_OR_RETURN_ERROR( loader != nil, Internal, "Cannot create Core AI model loader"); @@ -258,7 +266,7 @@ Error clear_cache_entry( "Core AI SDK cache entry absence confirmed: key=%s", key.UTF8String); } - ET_CHECK_OK_OR_RETURN_ERROR(maintenance.remove_staging()); + ET_CHECK_OK_OR_RETURN_ERROR(maintenance.remove_source()); ET_CHECK_OK_OR_RETURN_ERROR(maintenance.remove_bookmark()); ET_LOG(Info, "Core AI local cache entry cleared: key=%s", key.UTF8String); return Error::Ok; diff --git a/backends/apple/coreai/runtime/coreai_manifest.mm b/backends/apple/coreai/runtime/coreai_manifest.mm index 8d2a056d2df..af139c8f09d 100644 --- a/backends/apple/coreai/runtime/coreai_manifest.mm +++ b/backends/apple/coreai/runtime/coreai_manifest.mm @@ -42,7 +42,7 @@ : export_hash(manifest.export_hash), path(path), bundle_name(path.lastPathComponent), - digest(manifest.bundle_digests[bundle_name]), + digest(manifest.bundle_digests[path.lastPathComponent]), function(manifest.function), inputs(manifest.inputs), outputs(manifest.outputs), diff --git a/backends/apple/coreai/runtime/coreai_source_bundle.mm b/backends/apple/coreai/runtime/coreai_source_bundle.mm index 36cda105d5c..f52f4e2a38c 100644 --- a/backends/apple/coreai/runtime/coreai_source_bundle.mm +++ b/backends/apple/coreai/runtime/coreai_source_bundle.mm @@ -117,11 +117,11 @@ Error validate_tree( } Result inspect_source_bundle( - int root_fd, + int sources_fd, NSString* key, NSDictionary* files) { FileDescriptor entry( - open_directory_at(root_fd, key.fileSystemRepresentation)); + open_directory_at(sources_fd, key.fileSystemRepresentation)); if (entry.get() < 0) { if (errno == ENOENT) { return EntryState::Missing; @@ -142,69 +142,62 @@ FileDescriptor entry( return error; } -Error fully_sync_directory(int fd) { - ET_CHECK_OK_OR_RETURN_ERROR(sync_storage_directory(fd)); - ET_CHECK_OR_RETURN_ERROR( - fault_aware_retry( - FaultPoint::FullSync, [&] { return fcntl(fd, F_FULLFSYNC); }) == 0, - AccessFailed, - "Cannot fully synchronize Core AI source directory"); - return Error::Ok; -} - -struct StagingDirectory { +// Removes the unpublished build directory on every early return. +struct PendingDirectory { int parent_fd; NSString* name; bool published = false; - StagingDirectory(int parent_fd, NSString* name) + PendingDirectory(int parent_fd, NSString* name) : parent_fd(parent_fd), name(name) {} - StagingDirectory(const StagingDirectory&) = delete; - StagingDirectory& operator=(const StagingDirectory&) = delete; - StagingDirectory(StagingDirectory&& other) + PendingDirectory(const PendingDirectory&) = delete; + PendingDirectory& operator=(const PendingDirectory&) = delete; + PendingDirectory(PendingDirectory&& other) : parent_fd(other.parent_fd), name(other.name), published(other.published) { other.published = true; } - ~StagingDirectory() { + ~PendingDirectory() { if (published) { return; } const Error error = remove_storage_directory(parent_fd, name, StorageRemovalPolicy::Strict); if (error != Error::Ok) { - ET_LOG(Error, "Cannot remove Core AI staging directory"); + ET_LOG(Error, "Cannot remove pending Core AI source directory"); } } }; -Result create_staging_directory(int root_fd) { +Result create_pending_directory(int sources_fd) { NSString* name = - [@".staging-" stringByAppendingString:NSUUID.UUID.UUIDString]; + [@".pending-" stringByAppendingString:NSUUID.UUID.UUIDString]; ET_CHECK_OR_RETURN_ERROR( fault_aware_retry( - FaultPoint::CreateStagingDirectory, + FaultPoint::CreatePendingDirectory, [&] { - return mkdirat(root_fd, name.fileSystemRepresentation, 0777); + return mkdirat(sources_fd, name.fileSystemRepresentation, 0777); }) == 0, AccessFailed, - "Cannot create Core AI staging directory"); - return StagingDirectory{root_fd, [name copy]}; + "Cannot create pending Core AI source directory"); + return PendingDirectory{sources_fd, [name copy]}; } -Result open_staging_directory(int root_fd, NSString* name) { - const int fd = fault_aware_retry(FaultPoint::OpenStagingDirectory, [&] { - return open_directory_at(root_fd, name.fileSystemRepresentation); - }); +Result open_pending_directory(int sources_fd, NSString* name) { + FileDescriptor fd(fault_aware_retry(FaultPoint::OpenPendingDirectory, [&] { + return open_directory_at(sources_fd, name.fileSystemRepresentation); + })); ET_CHECK_OR_RETURN_ERROR( - fd >= 0, AccessFailed, "Cannot open Core AI staging directory"); + fd.get() >= 0, + AccessFailed, + "Cannot open pending Core AI source directory"); return fd; } -Error write_staging_bundle( - int staging_fd, +Error write_pending_bundle( + int pending_fd, const SelectedBundle& bundle, const runtime::NamedDataMap& named_data) { for (NSString* file in bundle.files) { @@ -226,28 +219,28 @@ Error write_staging_bundle( InvalidExternalData, "Invalid Core AI named data buffer"); ET_CHECK_OK_OR_RETURN_ERROR( - write_storage_file(staging_fd, file, buffer->data(), buffer->size())); + write_storage_file(pending_fd, file, buffer->data(), buffer->size())); } } - ET_CHECK_OK_OR_RETURN_ERROR(validate_tree(staging_fd, bundle.files)); - return fully_sync_directory(staging_fd); + ET_CHECK_OK_OR_RETURN_ERROR(validate_tree(pending_fd, bundle.files)); + return full_sync_storage_directory(pending_fd); } -Result publish_staging_directory( - int root_fd, - StagingDirectory& staging, +Result publish_pending_directory( + int sources_fd, + PendingDirectory& pending, NSString* key) { const int result = fault_aware_retry(FaultPoint::Rename, [&] { return renameatx_np( - root_fd, - staging.name.fileSystemRepresentation, - root_fd, + sources_fd, + pending.name.fileSystemRepresentation, + sources_fd, key.fileSystemRepresentation, RENAME_EXCL); }); if (result == 0) { - staging.published = true; - ET_CHECK_OK_OR_RETURN_ERROR(fully_sync_directory(root_fd)); + pending.published = true; + ET_CHECK_OK_OR_RETURN_ERROR(full_sync_storage_directory(sources_fd)); return true; } ET_CHECK_OR_RETURN_ERROR( @@ -256,10 +249,10 @@ Error write_staging_bundle( } Error recover_lost_publication_race( - int root_fd, + int sources_fd, NSString* key, NSDictionary* files) { - auto winner = inspect_source_bundle(root_fd, key, files); + auto winner = inspect_source_bundle(sources_fd, key, files); if (!winner.ok()) { return winner.error(); } @@ -267,7 +260,7 @@ Error recover_lost_publication_race( winner.get() == EntryState::Valid, InvalidExternalData, "Concurrently published Core AI source is incomplete"); - return fully_sync_directory(root_fd); + return full_sync_storage_directory(sources_fd); } } // namespace @@ -278,22 +271,22 @@ Error recover_lost_publication_race( const LockedCacheEntry& cache_entry, CorruptBundlePolicy corrupt_policy) { NSString* key = cache_entry.key(); - auto prepared = cache_entry.prepare_staging_directory(); - if (!prepared.ok()) { - return prepared.error(); + auto sources = cache_entry.prepare_source_directory(); + if (!sources.ok()) { + return sources.error(); } - NSURL* model_url = [[prepared->url URLByAppendingPathComponent:key - isDirectory:YES] + NSURL* model_url = [[sources->url URLByAppendingPathComponent:key + isDirectory:YES] URLByAppendingPathComponent:bundle.bundle_name isDirectory:YES]; - FileDescriptor root_fd = std::move(prepared->descriptor); + FileDescriptor sources_fd = std::move(sources->descriptor); - auto state = inspect_source_bundle(root_fd.get(), key, bundle.files); + auto state = inspect_source_bundle(sources_fd.get(), key, bundle.files); if (!state.ok()) { return state.error(); } if (state.get() == EntryState::Valid) { - ET_CHECK_OK_OR_RETURN_ERROR(fully_sync_directory(root_fd.get())); + ET_CHECK_OK_OR_RETURN_ERROR(full_sync_storage_directory(sources_fd.get())); return model_url; } if (state.get() == EntryState::Corrupt) { @@ -302,33 +295,32 @@ Error recover_lost_publication_race( InvalidExternalData, "Cannot replace a damaged Core AI stored bundle"); ET_CHECK_OK_OR_RETURN_ERROR(remove_storage_directory( - root_fd.get(), key, StorageRemovalPolicy::Maintenance)); + sources_fd.get(), key, StorageRemovalPolicy::Maintenance)); } ET_CHECK_OR_RETURN_ERROR( named_data != nullptr, InvalidProgram, "Missing NamedDataMap for Core AI source recovery"); - auto staging_result = create_staging_directory(root_fd.get()); - if (!staging_result.ok()) { - return staging_result.error(); + auto created = create_pending_directory(sources_fd.get()); + if (!created.ok()) { + return created.error(); } - StagingDirectory staging = std::move(staging_result.get()); - auto staging_fd_result = open_staging_directory(root_fd.get(), staging.name); - if (!staging_fd_result.ok()) { - return staging_fd_result.error(); + PendingDirectory pending = std::move(created.get()); + auto pending_fd = open_pending_directory(sources_fd.get(), pending.name); + if (!pending_fd.ok()) { + return pending_fd.error(); } - FileDescriptor staging_fd(staging_fd_result.get()); ET_CHECK_OK_OR_RETURN_ERROR( - write_staging_bundle(staging_fd.get(), bundle, *named_data)); + write_pending_bundle(pending_fd->get(), bundle, *named_data)); - auto published = publish_staging_directory(root_fd.get(), staging, key); + auto published = publish_pending_directory(sources_fd.get(), pending, key); if (!published.ok()) { return published.error(); } if (!published.get()) { ET_CHECK_OK_OR_RETURN_ERROR( - recover_lost_publication_race(root_fd.get(), key, bundle.files)); + recover_lost_publication_race(sources_fd.get(), key, bundle.files)); } return model_url; } diff --git a/backends/apple/coreai/runtime/coreai_storage.h b/backends/apple/coreai/runtime/coreai_storage.h index 16180b30800..b61699b6ff0 100644 --- a/backends/apple/coreai/runtime/coreai_storage.h +++ b/backends/apple/coreai/runtime/coreai_storage.h @@ -16,7 +16,16 @@ namespace executorch::backends::coreai { +enum class StorageCreationMode { + OpenExisting, + Create, +}; + runtime::Error sync_storage_directory(int fd); +// Also flushes the drive's write cache, which fsync alone does not on Apple +// platforms. +runtime::Error full_sync_storage_directory(int fd); + struct StorageDirectory final { std::optional descriptor; bool created; @@ -26,7 +35,8 @@ struct StorageDirectory final { runtime::Result open_storage_directory(int parent_fd, NSString* name, StorageCreationMode mode); // Returns an independently owned descriptor for a stable coordination file. -int open_storage_shared_file(int parent_fd, const char* name); +// On failure the descriptor is invalid and errno is set. +FileDescriptor open_storage_shared_file(int parent_fd, const char* name); // Creates a new file and missing parents, then syncs the file and its parent. // On failure, attempts to remove the new file; created parents may remain. runtime::Error write_storage_file( @@ -52,8 +62,8 @@ enum class StorageRemovalPolicy { Maintenance, }; -// Checks one child tree without following links or changing storage. Missing -// entries are already removed. Maintenance accepts malformed backend-owned +// Checks one child tree without following links or changing storage. A missing +// entry counts as removed. Maintenance accepts malformed backend-owned // non-directory entries. runtime::Error preflight_storage_directory( int parent_fd, @@ -62,7 +72,7 @@ runtime::Error preflight_storage_directory( // Removes one child directory without following links. The complete tree is // checked before removal and every entry is checked again during removal. -// Missing entries are already removed. Maintenance also removes malformed +// A missing entry counts as removed. Maintenance also removes malformed // backend-owned non-directory entries. runtime::Error remove_storage_directory( int parent_fd, diff --git a/backends/apple/coreai/runtime/coreai_storage.mm b/backends/apple/coreai/runtime/coreai_storage.mm index 79e28c8bde7..585152356d7 100644 --- a/backends/apple/coreai/runtime/coreai_storage.mm +++ b/backends/apple/coreai/runtime/coreai_storage.mm @@ -31,6 +31,11 @@ Full, }; +bool full_fsync(int fd) { + return fault_aware_retry( + FaultPoint::FullSync, [&] { return fcntl(fd, F_FULLFSYNC); }) == 0; +} + Error write_file( int root_fd, NSString* relative_path, @@ -56,8 +61,9 @@ FileDescriptor parent( for (NSUInteger i = 0; i + 1 < components.count; ++i) { auto opened = open_storage_directory( parent.get(), components[i], StorageCreationMode::Create); - if (!opened.ok()) + if (!opened.ok()) { return opened.error(); + } FileDescriptor next = std::move(*opened->descriptor); ET_CHECK_OK_OR_RETURN_ERROR(sync_storage_directory(parent.get())); parent = std::move(next); @@ -85,10 +91,7 @@ FileDescriptor file(retry_eintr([&] { ET_CHECK_OR_RETURN_ERROR( fault_aware_retry( FaultPoint::SyncFile, [&] { return fsync(file.get()); }) == 0 && - (sync_mode != StorageSyncMode::Full || - fault_aware_retry( - FaultPoint::FullSync, - [&] { return fcntl(file.get(), F_FULLFSYNC); }) == 0), + (sync_mode != StorageSyncMode::Full || full_fsync(file.get())), AccessFailed, "Cannot synchronize Core AI file"); ET_CHECK_OR_RETURN_ERROR( @@ -172,6 +175,35 @@ bool same_storage_entry(const struct stat& left, const struct stat& right) { (left.st_mode & S_IFMT) == (right.st_mode & S_IFMT); } +// Removes name only while it is still the entry described by expected. A +// missing entry counts as removed. Does not sync parent_fd. +Error remove_if_unchanged( + int parent_fd, + NSString* name, + const struct stat& expected) { + struct stat current; + const int inspected = + lstat_at(parent_fd, name.fileSystemRepresentation, ¤t); + if (inspected != 0 && errno == ENOENT) { + return Error::Ok; + } + ET_CHECK_OR_RETURN_ERROR( + inspected == 0 && same_storage_entry(expected, current), + AccessFailed, + "Core AI storage entry changed during removal"); + const int removed = fault_aware_retry(FaultPoint::Remove, [&] { + return unlinkat( + parent_fd, + name.fileSystemRepresentation, + S_ISDIR(expected.st_mode) ? AT_REMOVEDIR : 0); + }); + ET_CHECK_OR_RETURN_ERROR( + removed == 0 || errno == ENOENT, + AccessFailed, + "Cannot remove Core AI storage entry"); + return Error::Ok; +} + Error walk_storage_tree( int fd, StorageTreePass pass, @@ -208,25 +240,7 @@ FileDescriptor child( ET_CHECK_OK_OR_RETURN_ERROR(walk_storage_tree(child.get(), pass, policy)); } if (pass == StorageTreePass::Remove) { - struct stat current; - const int revalidated = - lstat_at(fd, name.fileSystemRepresentation, ¤t); - if (revalidated < 0 && errno == ENOENT) { - continue; - } - ET_CHECK_OR_RETURN_ERROR( - revalidated == 0 && same_storage_entry(info, current), - AccessFailed, - "Core AI storage entry changed during removal"); - const int removed = fault_aware_retry(FaultPoint::Remove, [&] { - return unlinkat( - fd, name.fileSystemRepresentation, directory ? AT_REMOVEDIR : 0); - }); - if (removed < 0 && errno == ENOENT) { - continue; - } - ET_CHECK_OR_RETURN_ERROR( - removed == 0, AccessFailed, "Cannot remove Core AI storage entry"); + ET_CHECK_OK_OR_RETURN_ERROR(remove_if_unchanged(fd, name, info)); } } return pass == StorageTreePass::Remove ? sync_storage_directory(fd) @@ -263,8 +277,9 @@ FileDescriptor child( created = made == 0; fd = open_directory(); } - if (fd < 0 && errno == ENOENT && !create) + if (fd < 0 && errno == ENOENT && !create) { return StorageDirectory{std::nullopt, false}; + } ET_CHECK_OR_RETURN_ERROR( fd >= 0, AccessFailed, "Cannot open Core AI storage directory"); return StorageDirectory{FileDescriptor(fd), created}; @@ -288,10 +303,12 @@ FileDescriptor copy(retry_eintr( errno = 0; dirent* entry = readdir(directory); if (entry == nullptr) { - if (errno == EINTR) + if (errno == EINTR) { continue; - if (errno != 0) + } + if (errno != 0) { result = Error::AccessFailed; + } break; } if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { @@ -341,51 +358,18 @@ Error remove_storage_directory( if (entry->state == StorageEntryState::Missing) { return Error::Ok; } - if (entry->state == StorageEntryState::NonDirectory) { - struct stat current; - const int revalidated = - lstat_at(parent_fd, name.fileSystemRepresentation, ¤t); - if (revalidated < 0 && errno == ENOENT) { - return Error::Ok; - } - ET_CHECK_OR_RETURN_ERROR( - revalidated == 0 && same_storage_entry(entry->info, current), - AccessFailed, - "Core AI storage entry changed during removal"); - const int removed = fault_aware_retry(FaultPoint::Remove, [&] { - return unlinkat(parent_fd, name.fileSystemRepresentation, 0); - }); - ET_CHECK_OR_RETURN_ERROR( - removed == 0 || (removed < 0 && errno == ENOENT), - AccessFailed, - "Cannot remove Core AI storage entry"); - return sync_storage_directory(parent_fd); - } - ET_CHECK_OK_OR_RETURN_ERROR(walk_storage_tree( - entry->directory->get(), StorageTreePass::Preflight, policy)); - ET_CHECK_OK_OR_RETURN_ERROR(walk_storage_tree( - entry->directory->get(), StorageTreePass::Remove, policy)); - struct stat current; - const int revalidated = - lstat_at(parent_fd, name.fileSystemRepresentation, ¤t); - if (revalidated < 0 && errno == ENOENT) { - return sync_storage_directory(parent_fd); + if (entry->state == StorageEntryState::Directory) { + ET_CHECK_OK_OR_RETURN_ERROR(walk_storage_tree( + entry->directory->get(), StorageTreePass::Preflight, policy)); + ET_CHECK_OK_OR_RETURN_ERROR(walk_storage_tree( + entry->directory->get(), StorageTreePass::Remove, policy)); } - ET_CHECK_OR_RETURN_ERROR( - revalidated == 0 && same_storage_entry(entry->info, current), - AccessFailed, - "Core AI storage directory changed during removal"); - const int removed = fault_aware_retry(FaultPoint::Remove, [&] { - return unlinkat(parent_fd, name.fileSystemRepresentation, AT_REMOVEDIR); - }); - ET_CHECK_OR_RETURN_ERROR( - removed == 0 || (removed < 0 && errno == ENOENT), - AccessFailed, - "Cannot remove Core AI storage directory"); + ET_CHECK_OK_OR_RETURN_ERROR( + remove_if_unchanged(parent_fd, name, entry->info)); return sync_storage_directory(parent_fd); } -int open_storage_shared_file(int parent_fd, const char* name) { +FileDescriptor open_storage_shared_file(int parent_fd, const char* name) { // APFS can return ENOENT to a concurrent O_CREAT loser without O_EXCL. int fd = retry_eintr([&] { return openat( @@ -399,7 +383,7 @@ int open_storage_shared_file(int parent_fd, const char* name) { return openat(parent_fd, name, O_RDWR | O_NOFOLLOW | O_CLOEXEC); }); } - return fd; + return FileDescriptor(fd); } Error sync_storage_directory(int fd) { @@ -411,6 +395,15 @@ Error sync_storage_directory(int fd) { return Error::Ok; } +Error full_sync_storage_directory(int fd) { + ET_CHECK_OK_OR_RETURN_ERROR(sync_storage_directory(fd)); + ET_CHECK_OR_RETURN_ERROR( + full_fsync(fd), + AccessFailed, + "Cannot fully synchronize Core AI directory"); + return Error::Ok; +} + Error write_storage_file( int root_fd, NSString* relative_path, @@ -443,9 +436,7 @@ Error publish_storage_data(int root_fd, NSString* name, NSData* data) { if (result == Error::Ok) { // Flush the renamed entry as well as the file's pre-rename contents. ET_CHECK_OR_RETURN_ERROR( - fault_aware_retry( - FaultPoint::FullSync, - [&] { return fcntl(root_fd, F_FULLFSYNC); }) == 0, + full_fsync(root_fd), AccessFailed, "Cannot fully synchronize Core AI publication directory"); return Error::Ok; diff --git a/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.h b/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.h index 75fe312d026..c0ac4d43281 100644 --- a/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.h +++ b/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.h @@ -8,7 +8,7 @@ #pragma once -#include "coreai_bookmark_fixture.h" +#include "coreai_cache_entry_fixture.h" #include "coreai_fake_loader.h" #include "coreai_fault_injection.h" #include "coreai_load_coordinator.h" diff --git a/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.mm b/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.mm index 51ef4ae665c..bb2c5708d35 100644 --- a/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.mm +++ b/backends/apple/coreai/runtime/test/coreai_acquisition_fixture.mm @@ -124,7 +124,7 @@ Error clear_test_cache_entry( auto key = test_cache_key(bundle); if (!key.ok()) return key.error(); - return [[[root URLByAppendingPathComponent:@"staging"] + return [[[root URLByAppendingPathComponent:@"sources"] URLByAppendingPathComponent:key.get()] URLByAppendingPathComponent:bundle.bundle_name]; } @@ -316,14 +316,8 @@ int acquisition_child_mode(int argc, char** argv) { auto selected = select_bundle(parsed.get(), @"arch_b", @"macOS"); if (!selected.ok()) return 5; - char* end = nullptr; - errno = 0; - const long descriptor = std::strtol(argv[4], &end, 10); - if (errno != 0 || end == argv[4] || *end != '\0' || descriptor < 0 || - descriptor > INT_MAX) - return 6; - const int fd = static_cast(descriptor); - if (fcntl(fd, F_GETFD) < 0) + const int fd = parse_child_fd(argv[4]); + if (fd < 0 || fcntl(fd, F_GETFD) < 0) return 6; signal(SIGPIPE, SIG_IGN); if (!send_child_byte(fd, 'C')) diff --git a/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm b/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm index 5ddb116a117..5b1df356756 100644 --- a/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_acquisition_test.mm @@ -66,7 +66,7 @@ - (void)specializeModelAtURL:(NSURL*)url void SetUp() override { ASSERT_NE(root.url, nil); - auto selected = bookmark_bundle(); + auto selected = selected_bundle(manifest_dict()); ASSERT_TRUE(selected.ok()); assets.emplace(SelectedHostBundle{selected.get(), @"macOS", @"arch_b"}); auto identity = test_cache_key(assets->bundle); @@ -90,14 +90,14 @@ Error acquire(BookmarkFakeLoader* loader, const NamedDataMap* named_data) { return bookmark.ok() ? bookmark.get() : nil; } - NSURL* staging_key_url() { - return [[[root.url URLByAppendingPathComponent:@"staging"] + NSURL* source_key_url() { + return [[[root.url URLByAppendingPathComponent:@"sources"] URLByAppendingPathComponent:key] copy]; } - BOOL staging_key_exists() { + BOOL source_key_exists() { return - [NSFileManager.defaultManager fileExistsAtPath:staging_key_url().path]; + [NSFileManager.defaultManager fileExistsAtPath:source_key_url().path]; } NSURL* lock_url() { @@ -122,7 +122,7 @@ ino_t lock_inode() { ASSERT_TRUE(staged.ok()); EXPECT_TRUE([fake.loader.lastSpecializeURL isEqual:staged.get().path]); EXPECT_TRUE([saved() isEqual:fake.loader.lastAcquired.bookmark]); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); EXPECT_FALSE( [NSFileManager.defaultManager fileExistsAtPath:staged.get().path]); } @@ -131,14 +131,14 @@ ino_t lock_inode() { BookmarkLoaderScope fake; ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); NSData* original = saved(); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); const int attempts = data.attempts.load(); ASSERT_EQ(acquire(fake.loader, nullptr), Error::Ok); EXPECT_EQ(fake.loader->specializations.load(), 1); EXPECT_EQ(fake.loader->restores.load(), 1); EXPECT_EQ(data.attempts.load(), attempts); EXPECT_TRUE([saved() isEqual:original]); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } TEST_F(CoreAIAcquisitionTest, RestoreErrorFailsClosedWithoutSourceFallback) { @@ -170,7 +170,7 @@ ino_t lock_inode() { NSData* replaced = saved(); EXPECT_NE(replaced, nil); EXPECT_FALSE([replaced isEqual:original]); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } TEST_F( @@ -223,7 +223,7 @@ ino_t lock_inode() { EXPECT_EQ(error, Error::InvalidArgument); EXPECT_TRUE(snapshot_matches(root.url, before)); EXPECT_EQ(saved(), nil); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } TEST_F(CoreAIAcquisitionTest, RestoreHitWithoutModelFailsClosed) { @@ -312,9 +312,7 @@ ino_t lock_inode() { EXPECT_EQ(saved(), nil); } -TEST_F( - CoreAIAcquisitionTest, - ColdSuccessRemovesWholeStagingAfterBookmarkFlush) { +TEST_F(CoreAIAcquisitionTest, ColdSuccessRemovesWholeSourceAfterBookmarkFlush) { BookmarkLoaderScope fake; __block std::vector ops; Error error = Error::Internal; @@ -328,7 +326,7 @@ ino_t lock_inode() { } ASSERT_EQ(error, Error::Ok); EXPECT_TRUE([saved() isEqual:fake.loader.lastAcquired.bookmark]); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); auto staged = acquisition_staged_url(root.url, assets->bundle); ASSERT_TRUE(staged.ok()); EXPECT_FALSE( @@ -421,13 +419,13 @@ ino_t lock_inode() { EXPECT_EQ(fake.loader->specializations.load(), specializations); const int attempts = data.attempts.load(); const int restores = fake.loader->restores.load(); - EXPECT_TRUE(staging_key_exists()); + EXPECT_TRUE(source_key_exists()); ASSERT_EQ(acquire(fake.loader, nullptr), Error::Ok); EXPECT_EQ(fake.loader->specializations.load(), specializations); EXPECT_EQ(fake.loader->restores.load(), restores + 1); EXPECT_EQ(data.attempts.load(), attempts); EXPECT_TRUE([saved() isEqual:bookmark]); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } TEST_F( @@ -451,13 +449,13 @@ ino_t lock_inode() { EXPECT_EQ(fake.loader->specializations.load(), specializations); const int attempts = data.attempts.load(); const int restores = fake.loader->restores.load(); - EXPECT_TRUE(staging_key_exists()); + EXPECT_TRUE(source_key_exists()); ASSERT_EQ(acquire(fake.loader, nullptr), Error::Ok); EXPECT_EQ(fake.loader->specializations.load(), specializations); EXPECT_EQ(fake.loader->restores.load(), restores + 1); EXPECT_EQ(data.attempts.load(), attempts); EXPECT_TRUE([saved() isEqual:bookmark]); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } TEST_F( @@ -485,13 +483,13 @@ ino_t lock_inode() { } const int attempts = data.attempts.load(); const int restores = fake.loader->restores.load(); - EXPECT_TRUE(staging_key_exists()); + EXPECT_TRUE(source_key_exists()); ASSERT_EQ(acquire(fake.loader, nullptr), Error::Ok); EXPECT_EQ(fake.loader->specializations.load(), specializations); EXPECT_EQ(fake.loader->restores.load(), restores + 1); EXPECT_EQ(data.attempts.load(), attempts); EXPECT_TRUE([saved() isEqual:bookmark]); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } TEST_F(CoreAIAcquisitionTest, WarmHitCleanupFailureStillReturnsModel) { @@ -519,7 +517,7 @@ ino_t lock_inode() { ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); EXPECT_EQ(fault.hits, 1); } - ASSERT_TRUE(staging_key_exists()); + ASSERT_TRUE(source_key_exists()); NSData* original = saved(); fake.loader.restoreMiss = YES; ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); @@ -528,7 +526,7 @@ ino_t lock_inode() { EXPECT_EQ(fake.loader->evictions.load(), 0); EXPECT_EQ(data.attempts.load(), 2); EXPECT_FALSE([saved() isEqual:original]); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } TEST_F( @@ -552,7 +550,7 @@ ino_t lock_inode() { EXPECT_EQ(fake.loader->evictions.load(), 0); EXPECT_TRUE([saved() isEqual:original]); ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } TEST_F(CoreAIAcquisitionTest, MissingNamedDataIsNotRetried) { @@ -594,7 +592,7 @@ ino_t lock_inode() { ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); original = saved(); ASSERT_NE(original, nil); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); fake.loader.restoreMiss = YES; } { @@ -613,7 +611,7 @@ ino_t lock_inode() { fake.loader.restoreMiss = NO; ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); EXPECT_EQ(fake.loader->specializations.load(), calls + (prior ? 0 : 1)); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } INSTANTIATE_TEST_SUITE_P( Boundaries, @@ -654,22 +652,22 @@ void expect_parallel(bool same_key) { expect_parallel(false); } -TEST_F(CoreAIAcquisitionProcessTest, CrashBeforeSdkRetainsStagingAndRetries) { +TEST_F(CoreAIAcquisitionProcessTest, CrashBeforeSdkRetainsSourceAndRetries) { BookmarkChild child; ASSERT_TRUE(spawn_acquisition_crash( child, root.url, FaultPoint::BeforeSDK, manifest_dict())); ASSERT_TRUE(child.receive('C')); ASSERT_TRUE(child.expect_exit(73)); EXPECT_EQ(saved(), nil); - EXPECT_TRUE(staging_key_exists()); + EXPECT_TRUE(source_key_exists()); BookmarkLoaderScope fake; ASSERT_EQ(acquire(fake.loader, &data), Error::Ok); EXPECT_EQ(fake.loader->specializations.load(), 1); EXPECT_EQ(data.attempts.load(), 0); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } -TEST_F(CoreAIAcquisitionProcessTest, CrashAfterSdkRetainsStagingAndRetries) { +TEST_F(CoreAIAcquisitionProcessTest, CrashAfterSdkRetainsSourceAndRetries) { BookmarkChild child; ASSERT_TRUE(spawn_acquisition_crash( child, root.url, FaultPoint::AfterSDK, manifest_dict())); @@ -686,7 +684,7 @@ void expect_parallel(bool same_key) { EXPECT_EQ(fake.loader->restores.load(), 0); EXPECT_EQ(data.attempts.load(), 0); EXPECT_NE(saved(), nil); - EXPECT_FALSE(staging_key_exists()); + EXPECT_FALSE(source_key_exists()); } class CoreAILifecycleLogTest : public CoreAIAcquisitionTest { diff --git a/backends/apple/coreai/runtime/test/coreai_bookmark_contention_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_entry_contention_test.mm similarity index 98% rename from backends/apple/coreai/runtime/test/coreai_bookmark_contention_test.mm rename to backends/apple/coreai/runtime/test/coreai_cache_entry_contention_test.mm index 4a38473a1c6..cd0220a1b71 100644 --- a/backends/apple/coreai/runtime/test/coreai_bookmark_contention_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_entry_contention_test.mm @@ -11,7 +11,7 @@ #include #include #include -#include "coreai_bookmark_fixture.h" +#include "coreai_cache_entry_fixture.h" #include "coreai_fault_scope.h" #include "coreai_filesystem_fixture.h" @@ -66,7 +66,7 @@ CacheEntryWaiter start_cache_entry_waiter( void SetUp() override { ASSERT_NE(root.url, nil); - auto selected = bookmark_bundle(); + auto selected = selected_bundle(manifest_dict()); ASSERT_TRUE(selected.ok()); bundle = std::make_unique(selected.get()); auto result = test_cache_key(*bundle); @@ -200,7 +200,7 @@ void SetUp() override { ASSERT_TRUE(holder.spawn(@"--bookmark-hold", root.url.path, @"same")); ASSERT_TRUE(holder.receive('L')); NSURL* file = lock_url(); - struct stat before {}; + struct stat before{}; ASSERT_EQ(stat(file.fileSystemRepresentation, &before), 0); BookmarkChild waiter; ASSERT_TRUE(waiter.spawn(@"--bookmark-lock", root.url.path, @"same")); @@ -213,7 +213,7 @@ void SetUp() override { ASSERT_TRUE(holder.kill_and_reap()); ASSERT_TRUE(waiter.receive('L')); ASSERT_TRUE(waiter.expect_exit(0)); - struct stat after {}; + struct stat after{}; ASSERT_EQ(stat(file.fileSystemRepresentation, &after), 0); EXPECT_EQ(before.st_dev, after.st_dev); EXPECT_EQ(before.st_ino, after.st_ino); @@ -225,7 +225,7 @@ void SetUp() override { BookmarkChild holder; ASSERT_TRUE(holder.spawn(@"--bookmark-hold", root.url.path, @"same")); ASSERT_TRUE(holder.receive('L')); - struct stat before {}; + struct stat before{}; ASSERT_EQ(lstat(lock_url().fileSystemRepresentation, &before), 0); BookmarkChild waiter; ASSERT_TRUE(waiter.spawn(@"--bookmark-lock", root.url.path, @"collision")); @@ -234,7 +234,7 @@ void SetUp() override { ASSERT_TRUE(holder.kill_and_reap()); ASSERT_TRUE(waiter.receive('L')); ASSERT_TRUE(waiter.expect_exit(0)); - struct stat after {}; + struct stat after{}; ASSERT_EQ(lstat(lock_url().fileSystemRepresentation, &after), 0); EXPECT_EQ(before.st_dev, after.st_dev); EXPECT_EQ(before.st_ino, after.st_ino); diff --git a/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.h b/backends/apple/coreai/runtime/test/coreai_cache_entry_fixture.h similarity index 94% rename from backends/apple/coreai/runtime/test/coreai_bookmark_fixture.h rename to backends/apple/coreai/runtime/test/coreai_cache_entry_fixture.h index ea55fa3d711..40dee635fdb 100644 --- a/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.h +++ b/backends/apple/coreai/runtime/test/coreai_cache_entry_fixture.h @@ -15,10 +15,10 @@ #include "coreai_cache_entry.h" #include "coreai_cache_key.h" #include "coreai_manifest.h" +#include "coreai_manifest_fixture.h" namespace executorch::backends::coreai::testing { -runtime::Result bookmark_bundle(bool aot = false); runtime::Result test_cache_directory( NSString* path, StorageCreationMode mode = StorageCreationMode::Create); @@ -40,6 +40,8 @@ NSURL* test_cache_lock_url(NSURL* root, NSString* key); runtime::Result saved_bookmark(NSURL* root, NSString* key); runtime::Result sized_bookmark(size_t size, uint32_t tag = 1); +// Returns -1 unless argument is a nonnegative int in decimal. +int parse_child_fd(const char* argument); bool send_child_byte(int fd, char byte); void set_bookmark_test_executable(const char* path); // Returns -1 for normal test invocations, otherwise a child-mode exit code. diff --git a/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.mm b/backends/apple/coreai/runtime/test/coreai_cache_entry_fixture.mm similarity index 95% rename from backends/apple/coreai/runtime/test/coreai_bookmark_fixture.mm rename to backends/apple/coreai/runtime/test/coreai_cache_entry_fixture.mm index 0d06c71e9c1..f5d6b82a661 100644 --- a/backends/apple/coreai/runtime/test/coreai_bookmark_fixture.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_entry_fixture.mm @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. */ -#include "coreai_bookmark_fixture.h" +#include "coreai_cache_entry_fixture.h" #include #include @@ -22,6 +22,7 @@ #include #include #include "coreai_fault_scope.h" +#include "coreai_filesystem_fixture.h" #include "coreai_manifest_fixture.h" extern char** environ; @@ -68,14 +69,10 @@ bool send_child_byte_impl(int fd, char byte) { } } // namespace -Result bookmark_bundle(bool aot) { - return selected_bundle(aot ? aot_manifest_dict() : manifest_dict()); -} - Result test_cache_directory( NSString* path, StorageCreationMode mode) { - auto directory = resolve_cache_directory(path, mode); + auto directory = resolve_test_cache_directory(path, mode); if (!directory.ok()) { return directory.error(); } @@ -99,7 +96,7 @@ bool send_child_byte_impl(int fd, char byte) { NSString* path, NSString* key) { auto directory = - resolve_cache_directory(path, StorageCreationMode::OpenExisting); + resolve_test_cache_directory(path, StorageCreationMode::OpenExisting); if (!directory.ok()) { return directory.error(); } @@ -109,6 +106,17 @@ bool send_child_byte_impl(int fd, char byte) { return LockedCacheEntry::acquire_existing(directory->value(), key); } +int parse_child_fd(const char* argument) { + char* end = nullptr; + errno = 0; + const long value = std::strtol(argument, &end, 10); + if (errno != 0 || end == argument || *end != '\0' || value < 0 || + value > INT_MAX) { + return -1; + } + return static_cast(value); +} + bool send_child_byte(int fd, char byte) { return send_child_byte_impl(fd, byte); } @@ -207,14 +215,11 @@ int bookmark_child_mode(int argc, char** argv) { if ((!hold && !lock) || (!other && !same && !collision)) { return 2; } - char* end = nullptr; - errno = 0; - const long value = std::strtol(argv[4], &end, 10); - if (errno != 0 || end == argv[4] || *end != '\0' || value < 0 || - value > INT_MAX) { + const int parsed_fd = parse_child_fd(argv[4]); + if (parsed_fd < 0) { return 3; } - FileDescriptor signal_fd(static_cast(value)); + FileDescriptor signal_fd(parsed_fd); if (fcntl(signal_fd.get(), F_GETFD) < 0) { return 4; } @@ -223,7 +228,7 @@ int bookmark_child_mode(int argc, char** argv) { if (root == nil) { return 5; } - auto bundle = bookmark_bundle(); + auto bundle = selected_bundle(manifest_dict()); if (!bundle.ok()) { return 6; } diff --git a/backends/apple/coreai/runtime/test/coreai_bookmark_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_entry_test.mm similarity index 82% rename from backends/apple/coreai/runtime/test/coreai_bookmark_test.mm rename to backends/apple/coreai/runtime/test/coreai_cache_entry_test.mm index 974a12a02b3..b277344b27a 100644 --- a/backends/apple/coreai/runtime/test/coreai_bookmark_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_entry_test.mm @@ -9,7 +9,7 @@ #include #include #include -#include "coreai_bookmark_fixture.h" +#include "coreai_cache_entry_fixture.h" #include "coreai_fault_scope.h" #include "coreai_filesystem_fixture.h" #include "coreai_storage.h" @@ -29,7 +29,7 @@ void SetUp() override { ASSERT_NE(root.url, nil); - auto selected = bookmark_bundle(); + auto selected = selected_bundle(manifest_dict()); ASSERT_TRUE(selected.ok()); bundle = std::make_unique(selected.get()); auto result = test_cache_key(*bundle); @@ -183,7 +183,7 @@ FileDescriptor file(open( EXPECT_EQ(info.st_size, static_cast(8 * kMiB + 1)); } -TEST_F(CoreAIBookmarkIOTest, ExistingLookupReadsWithoutPreparingStaging) { +TEST_F(CoreAIBookmarkIOTest, ExistingLookupReadsWithoutPreparingSources) { NSData* bytes = [@"bookmark" dataUsingEncoding:NSUTF8StringEncoding]; ASSERT_EQ(locked->write_bookmark(bytes), Error::Ok); locked.reset(); @@ -194,40 +194,40 @@ FileDescriptor file(open( existing.get()->persistence(), CacheEntryPersistence::BookmarkCapable); EXPECT_TRUE(bookmark_bytes_equal(*existing.get(), bytes)); EXPECT_FALSE([NSFileManager.defaultManager - fileExistsAtPath:[root.url URLByAppendingPathComponent:@"staging"].path]); + fileExistsAtPath:[root.url URLByAppendingPathComponent:@"sources"].path]); } -TEST_F(CoreAIBookmarkIOTest, PreparesSharedStagingParentOnDemand) { +TEST_F(CoreAIBookmarkIOTest, PreparesSharedSourcesParentOnDemand) { EXPECT_TRUE([locked->key() isEqual:key]); NSString* expected = [root.url.URLByResolvingSymlinksInPath - URLByAppendingPathComponent:@"staging"] + URLByAppendingPathComponent:@"sources"] .path; EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:expected]); - auto staging = locked->prepare_staging_directory(); - ASSERT_TRUE(staging.ok()); - EXPECT_TRUE([staging->url.path isEqual:expected]); + auto sources = locked->prepare_source_directory(); + ASSERT_TRUE(sources.ok()); + EXPECT_TRUE([sources->url.path isEqual:expected]); struct stat info {}; - ASSERT_EQ(lstat(staging->url.fileSystemRepresentation, &info), 0); + ASSERT_EQ(lstat(sources->url.fileSystemRepresentation, &info), 0); EXPECT_TRUE(S_ISDIR(info.st_mode)); - auto repeated = locked->prepare_staging_directory(); + auto repeated = locked->prepare_source_directory(); ASSERT_TRUE(repeated.ok()); EXPECT_TRUE([repeated->url.path isEqual:expected]); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:[expected stringByAppendingPathComponent:key]]); } -TEST_F(CoreAIBookmarkIOTest, CleanupDoesNotCreateMissingStaging) { +TEST_F(CoreAIBookmarkIOTest, CleanupDoesNotCreateMissingSources) { StorageFaultScope fault(FaultPoint::BeforeCreateDirectory); - EXPECT_EQ(locked->remove_staging(), Error::Ok); + EXPECT_EQ(locked->remove_source(), Error::Ok); EXPECT_EQ(fault.matches, 0); EXPECT_FALSE([NSFileManager.defaultManager - fileExistsAtPath:[root.url URLByAppendingPathComponent:@"staging"].path]); + fileExistsAtPath:[root.url URLByAppendingPathComponent:@"sources"].path]); } TEST_F(CoreAIBookmarkIOTest, CleanupRejectsUnexpectedTypesBeforeDeletingFiles) { - auto staging = locked->prepare_staging_directory(); - ASSERT_TRUE(staging.ok()); - NSString* entry = [staging->url.path stringByAppendingPathComponent:key]; + auto sources = locked->prepare_source_directory(); + ASSERT_TRUE(sources.ok()); + NSString* entry = [sources->url.path stringByAppendingPathComponent:key]; ASSERT_EQ(mkdir(entry.fileSystemRepresentation, 0700), 0); NSString* file = [entry stringByAppendingPathComponent:@"data"]; NSData* bytes = [@"preserved" dataUsingEncoding:NSUTF8StringEncoding]; @@ -235,7 +235,7 @@ FileDescriptor file(open( NSString* pipe = [entry stringByAppendingPathComponent:@"pipe"]; ASSERT_EQ(mkfifo(pipe.fileSystemRepresentation, 0600), 0); - EXPECT_EQ(locked->remove_staging(), Error::InvalidExternalData); + EXPECT_EQ(locked->remove_source(), Error::InvalidExternalData); EXPECT_TRUE([[NSData dataWithContentsOfFile:file] isEqual:bytes]); struct stat info; ASSERT_EQ(lstat(pipe.fileSystemRepresentation, &info), 0); @@ -243,9 +243,9 @@ FileDescriptor file(open( } TEST_F(CoreAIBookmarkIOTest, CleanupUsesLockedRootAfterPathReplacement) { - auto staging = locked->prepare_staging_directory(); - ASSERT_TRUE(staging.ok()); - NSString* entry = [staging->url.path stringByAppendingPathComponent:key]; + auto sources = locked->prepare_source_directory(); + ASSERT_TRUE(sources.ok()); + NSString* entry = [sources->url.path stringByAppendingPathComponent:key]; ASSERT_EQ(mkdir(entry.fileSystemRepresentation, 0700), 0); TestDirectory moved; ASSERT_NE(moved.url, nil); @@ -254,15 +254,15 @@ FileDescriptor file(open( root.url.fileSystemRepresentation, moved.url.fileSystemRepresentation), 0); - NSURL* replacement = [[root.url URLByAppendingPathComponent:@"staging"] + NSURL* replacement = [[root.url URLByAppendingPathComponent:@"sources"] URLByAppendingPathComponent:key]; ASSERT_TRUE([NSFileManager.defaultManager createDirectoryAtURL:replacement withIntermediateDirectories:YES attributes:nil error:nil]); - EXPECT_EQ(locked->remove_staging(), Error::Ok); - NSURL* original = [[moved.url URLByAppendingPathComponent:@"staging"] + EXPECT_EQ(locked->remove_source(), Error::Ok); + NSURL* original = [[moved.url URLByAppendingPathComponent:@"sources"] URLByAppendingPathComponent:key]; EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:original.path]); EXPECT_TRUE([NSFileManager.defaultManager fileExistsAtPath:replacement.path]); @@ -273,8 +273,8 @@ FileDescriptor file(open( ASSERT_EQ(mkdir(reference.fileSystemRepresentation, 0777), 0); struct stat expected; ASSERT_EQ(stat(reference.fileSystemRepresentation, &expected), 0); - ASSERT_TRUE(locked->prepare_staging_directory().ok()); - for (NSString* name in @[ @"locks", @"bookmarks", @"staging" ]) { + ASSERT_TRUE(locked->prepare_source_directory().ok()); + for (NSString* name in @[ @"locks", @"bookmarks", @"sources" ]) { NSURL* directory = [root.url URLByAppendingPathComponent:name]; struct stat actual; ASSERT_EQ(stat(directory.fileSystemRepresentation, &actual), 0); @@ -283,7 +283,7 @@ FileDescriptor file(open( } TEST_F(CoreAIBookmarkIOTest, ReusesDirectoriesWithoutCreation) { - ASSERT_TRUE(locked->prepare_staging_directory().ok()); + ASSERT_TRUE(locked->prepare_source_directory().ok()); ASSERT_EQ( locked->write_bookmark( [@"bookmark" dataUsingEncoding:NSUTF8StringEncoding]), @@ -293,57 +293,57 @@ FileDescriptor file(open( auto created = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(created.ok()); ASSERT_NE(created.get(), nullptr); - EXPECT_TRUE(created.get()->prepare_staging_directory().ok()); + EXPECT_TRUE(created.get()->prepare_source_directory().ok()); created.get().reset(); auto existing = acquire_existing_test_cache_entry(root.url.path, key); ASSERT_TRUE(existing.ok()); ASSERT_NE(existing.get(), nullptr); - EXPECT_TRUE(existing.get()->prepare_staging_directory().ok()); + EXPECT_TRUE(existing.get()->prepare_source_directory().ok()); EXPECT_EQ(fault.matches, 0); } -TEST_F(CoreAIBookmarkIOTest, RecreatesMissingStagingParent) { - auto staging = locked->prepare_staging_directory(); - ASSERT_TRUE(staging.ok()); - ASSERT_EQ(rmdir(staging->url.fileSystemRepresentation), 0); +TEST_F(CoreAIBookmarkIOTest, RecreatesMissingSourcesParent) { + auto sources = locked->prepare_source_directory(); + ASSERT_TRUE(sources.ok()); + ASSERT_EQ(rmdir(sources->url.fileSystemRepresentation), 0); StorageFaultScope fault(FaultPoint::BeforeCreateDirectory); - EXPECT_EQ(locked->prepare_staging_directory().error(), Error::AccessFailed); + EXPECT_EQ(locked->prepare_source_directory().error(), Error::AccessFailed); EXPECT_EQ(fault.hits, 1); - auto recreated = locked->prepare_staging_directory(); + auto recreated = locked->prepare_source_directory(); ASSERT_TRUE(recreated.ok()); - EXPECT_TRUE([recreated->url isEqual:staging->url]); + EXPECT_TRUE([recreated->url isEqual:sources->url]); } -TEST_F(CoreAIBookmarkIOTest, AcceptsConcurrentStagingCreation) { - NSURL* staging = [root.url URLByAppendingPathComponent:@"staging"]; +TEST_F(CoreAIBookmarkIOTest, AcceptsConcurrentSourcesCreation) { + NSURL* sources = [root.url URLByAppendingPathComponent:@"sources"]; StorageFaultScope fault(FaultPoint::BeforeCreateDirectory); fault.armed = false; __block int creations = 0; fault.observe = ^(FaultPoint point) { if (point == FaultPoint::BeforeCreateDirectory) { ++creations; - EXPECT_EQ(mkdir(staging.fileSystemRepresentation, 0700), 0); + EXPECT_EQ(mkdir(sources.fileSystemRepresentation, 0700), 0); } }; - EXPECT_TRUE(locked->prepare_staging_directory().ok()); + EXPECT_TRUE(locked->prepare_source_directory().ok()); EXPECT_EQ(creations, 1); } -TEST_F(CoreAIBookmarkIOTest, RejectsInvalidStagingParent) { +TEST_F(CoreAIBookmarkIOTest, RejectsInvalidSourcesParent) { TestDirectory target; ASSERT_NE(target.url, nil); - NSURL* staging = [root.url URLByAppendingPathComponent:@"staging"]; + NSURL* sources = [root.url URLByAppendingPathComponent:@"sources"]; ASSERT_EQ( symlink( target.url.fileSystemRepresentation, - staging.fileSystemRepresentation), + sources.fileSystemRepresentation), 0); - EXPECT_EQ(locked->prepare_staging_directory().error(), Error::AccessFailed); - ASSERT_EQ(unlink(staging.fileSystemRepresentation), 0); + EXPECT_EQ(locked->prepare_source_directory().error(), Error::AccessFailed); + ASSERT_EQ(unlink(sources.fileSystemRepresentation), 0); NSData* bytes = [@"not a directory" dataUsingEncoding:NSUTF8StringEncoding]; - ASSERT_TRUE([bytes writeToURL:staging atomically:NO]); - EXPECT_EQ(locked->prepare_staging_directory().error(), Error::AccessFailed); - EXPECT_TRUE([[NSData dataWithContentsOfURL:staging] isEqual:bytes]); + ASSERT_TRUE([bytes writeToURL:sources atomically:NO]); + EXPECT_EQ(locked->prepare_source_directory().error(), Error::AccessFailed); + EXPECT_TRUE([[NSData dataWithContentsOfURL:sources] isEqual:bytes]); } TEST_F(CoreAIBookmarkPathTest, AllowsTemporaryRootsAndRejectsInvalidKeys) { @@ -362,25 +362,25 @@ FileDescriptor file(open( ASSERT_NE(locked.get(), nullptr); [path setString:other.url.path]; - auto staging = locked.get()->prepare_staging_directory(); - ASSERT_TRUE(staging.ok()); + auto sources = locked.get()->prepare_source_directory(); + ASSERT_TRUE(sources.ok()); NSString* expected = [root.url.URLByResolvingSymlinksInPath - URLByAppendingPathComponent:@"staging"] + URLByAppendingPathComponent:@"sources"] .path; - NSString* unrelated = [other.url URLByAppendingPathComponent:@"staging"].path; + NSString* unrelated = [other.url URLByAppendingPathComponent:@"sources"].path; EXPECT_TRUE([NSFileManager.defaultManager fileExistsAtPath:expected]); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:unrelated]); - EXPECT_TRUE([staging->url.path isEqual:expected]) - << "Returned staging path: " << staging->url.path.UTF8String - << "; descriptor-pinned staging path: " << expected.UTF8String; + EXPECT_TRUE([sources->url.path isEqual:expected]) + << "Returned sources path: " << sources->url.path.UTF8String + << "; descriptor-pinned sources path: " << expected.UTF8String; } -TEST_F(CoreAIBookmarkPathTest, ExcludesRootWithoutCreatingStaging) { +TEST_F(CoreAIBookmarkPathTest, ExcludesRootWithoutCreatingSources) { auto locked = acquire_test_cache_entry(root.url.path, key); ASSERT_TRUE(locked.ok()); EXPECT_TRUE(backup_excluded(root.url)); EXPECT_FALSE([NSFileManager.defaultManager - fileExistsAtPath:[root.url URLByAppendingPathComponent:@"staging"].path]); + fileExistsAtPath:[root.url URLByAppendingPathComponent:@"sources"].path]); } TEST_F(CoreAIBookmarkPathTest, MissingLookupLeavesRootAndEntryUntouched) { @@ -389,7 +389,7 @@ FileDescriptor file(open( for (NSString* path in @[ root.url.path, missing ]) { SCOPED_TRACE(path.UTF8String); auto resolved = - resolve_cache_directory(path, StorageCreationMode::OpenExisting); + resolve_test_cache_directory(path, StorageCreationMode::OpenExisting); ASSERT_TRUE(resolved.ok()); const bool root_exists = [path isEqual:root.url.path]; EXPECT_EQ(resolved->has_value(), root_exists); @@ -424,8 +424,8 @@ FileDescriptor file(open( EXPECT_TRUE(bookmark_bytes_equal(*other.get(), bytes)); } -TEST_F(CoreAIBookmarkPathTest, StagingOnlyLookupHasExplicitPersistence) { - NSURL* staged = [[root.url URLByAppendingPathComponent:@"staging"] +TEST_F(CoreAIBookmarkPathTest, SourceOnlyLookupHasExplicitPersistence) { + NSURL* staged = [[root.url URLByAppendingPathComponent:@"sources"] URLByAppendingPathComponent:key]; ASSERT_TRUE([NSFileManager.defaultManager createDirectoryAtURL:staged withIntermediateDirectories:YES @@ -434,7 +434,7 @@ FileDescriptor file(open( auto existing = acquire_existing_test_cache_entry(root.url.path, key); ASSERT_TRUE(existing.ok()); ASSERT_NE(existing.get(), nullptr); - EXPECT_EQ(existing.get()->persistence(), CacheEntryPersistence::StagingOnly); + EXPECT_EQ(existing.get()->persistence(), CacheEntryPersistence::SourceOnly); EXPECT_TRUE(bookmark_bytes_equal(*existing.get(), nil)); NSData* bytes = [@"bookmark" dataUsingEncoding:NSUTF8StringEncoding]; EXPECT_EQ(existing.get()->write_bookmark(bytes), Error::InvalidArgument); @@ -447,7 +447,8 @@ FileDescriptor file(open( TEST_F(CoreAIBookmarkPathTest, ResolvesAndCreatesRootWithBackupExclusion) { ASSERT_TRUE(set_backup_excluded(root.url, false)); NSString* path = [root.url URLByAppendingPathComponent:@"nested/cache"].path; - auto resolved = resolve_cache_directory(path, StorageCreationMode::Create); + auto resolved = + resolve_test_cache_directory(path, StorageCreationMode::Create); ASSERT_TRUE(resolved.ok()); ASSERT_TRUE(resolved->has_value()); EXPECT_TRUE([resolved->value().path @@ -455,7 +456,7 @@ FileDescriptor file(open( EXPECT_TRUE(backup_excluded([NSURL fileURLWithPath:resolved->value().path])); EXPECT_FALSE(backup_excluded(root.url)); auto existing = - resolve_cache_directory(path, StorageCreationMode::OpenExisting); + resolve_test_cache_directory(path, StorageCreationMode::OpenExisting); ASSERT_TRUE(existing.ok()); ASSERT_TRUE(existing->has_value()); EXPECT_TRUE([existing->value().path @@ -469,19 +470,21 @@ FileDescriptor file(open( @[ @"relative", @"~", @"~/cache", @"~nobody/cache" ]) { SCOPED_TRACE(path.UTF8String); EXPECT_EQ( - resolve_cache_directory(path, mode).error(), Error::InvalidArgument); + resolve_test_cache_directory(path, mode).error(), + Error::InvalidArgument); } EXPECT_EQ( - resolve_cache_directory(nil, mode).error(), Error::InvalidArgument); + resolve_test_cache_directory(nil, mode).error(), + Error::InvalidArgument); } NSURL* file = [root.url URLByAppendingPathComponent:@"file"]; ASSERT_TRUE([[NSData data] writeToURL:file atomically:NO]); EXPECT_EQ( - resolve_cache_directory(file.path, StorageCreationMode::OpenExisting) + resolve_test_cache_directory(file.path, StorageCreationMode::OpenExisting) .error(), Error::AccessFailed); EXPECT_EQ( - resolve_cache_directory( + resolve_test_cache_directory( [file.path stringByAppendingPathComponent:@"child"], StorageCreationMode::OpenExisting) .error(), diff --git a/backends/apple/coreai/runtime/test/coreai_cache_key_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_key_test.mm index 19206651539..03fe45ca4a0 100644 --- a/backends/apple/coreai/runtime/test/coreai_cache_key_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_key_test.mm @@ -10,7 +10,7 @@ #include #include -#include "coreai_bookmark_fixture.h" +#include "coreai_cache_entry_fixture.h" #include "coreai_manifest_fixture.h" using namespace executorch::runtime; @@ -30,7 +30,7 @@ } TEST(CoreAICacheKeyIdentityTest, IgnoresBindingsAndDeliveryLocation) { - auto baseline = bookmark_bundle(); + auto baseline = selected_bundle(manifest_dict()); ASSERT_TRUE(baseline.ok()); auto expected = test_cache_key(baseline.get()); ASSERT_TRUE(expected.ok()); @@ -49,7 +49,7 @@ } TEST(CoreAICacheKeyIdentityTest, SeparatesExportedBundleIdentity) { - auto baseline = bookmark_bundle(); + auto baseline = selected_bundle(manifest_dict()); ASSERT_TRUE(baseline.ok()); auto expected = test_cache_key(baseline.get()); ASSERT_TRUE(expected.ok()); @@ -60,7 +60,7 @@ auto reweighted = selected_bundle(digest_changed); ASSERT_TRUE(reweighted.ok()); - auto aot = bookmark_bundle(true); + auto aot = selected_bundle(aot_manifest_dict()); ASSERT_TRUE(aot.ok()); for (const SelectedBundle* bundle : {&reweighted.get(), &aot.get()}) { auto actual = test_cache_key(*bundle); @@ -97,7 +97,7 @@ } TEST(CoreAICacheKeyIdentityTest, SeparatesDeviceAndSpecializationIdentity) { - auto bundle = bookmark_bundle(); + auto bundle = selected_bundle(manifest_dict()); ASSERT_TRUE(bundle.ok()); auto baseline = test_cache_key(bundle.get()); auto platform = @@ -119,7 +119,7 @@ } TEST(CoreAICacheKeyIdentityTest, RejectsIncompletePlainIdentity) { - auto bundle = bookmark_bundle(); + auto bundle = selected_bundle(manifest_dict()); ASSERT_TRUE(bundle.ok()); EXPECT_FALSE( cache_variant_key(bundle.get(), nil, @"arch_b", @"default").ok()); @@ -184,7 +184,7 @@ } TEST(CoreAICacheKeyFormatTest, LockStripesStayWithinConfiguredPool) { - auto bundle = bookmark_bundle(); + auto bundle = selected_bundle(manifest_dict()); ASSERT_TRUE(bundle.ok()); NSMutableSet* names = [NSMutableSet set]; for (NSUInteger index = 0; index < 4096; ++index) { diff --git a/backends/apple/coreai/runtime/test/coreai_cache_root_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_root_test.mm index 04235d45a83..5fffbce9a6e 100644 --- a/backends/apple/coreai/runtime/test/coreai_cache_root_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_root_test.mm @@ -52,7 +52,7 @@ ASSERT_TRUE(set_backup_excluded(parent, false)); NSURL* root = [parent URLByAppendingPathComponent:@"nested/models"]; auto prepared = - resolve_cache_directory(root.path, StorageCreationMode::Create); + resolve_test_cache_directory(root.path, StorageCreationMode::Create); ASSERT_TRUE(prepared.ok()); ASSERT_TRUE(prepared->has_value()); EXPECT_TRUE([prepared->value().path @@ -62,25 +62,27 @@ ASSERT_TRUE((!backup_excluded(root.URLByDeletingLastPathComponent))); ASSERT_TRUE(set_backup_excluded(root, false)); - auto inspected = - resolve_cache_directory(root.path, StorageCreationMode::OpenExisting); + auto inspected = resolve_test_cache_directory( + root.path, StorageCreationMode::OpenExisting); ASSERT_TRUE(inspected.ok()); ASSERT_TRUE(inspected->has_value()); EXPECT_TRUE([inspected->value().path isEqualToString:root.URLByResolvingSymlinksInPath.path]); ASSERT_TRUE((!backup_excluded(root))); ASSERT_TRUE( - (resolve_cache_directory(root.path, StorageCreationMode::Create).ok() && + (resolve_test_cache_directory(root.path, StorageCreationMode::Create) + .ok() && backup_excluded(root))); NSURL* missing = [sandbox.url URLByAppendingPathComponent:@"missing"]; - auto absent = - resolve_cache_directory(missing.path, StorageCreationMode::OpenExisting); + auto absent = resolve_test_cache_directory( + missing.path, StorageCreationMode::OpenExisting); ASSERT_TRUE(absent.ok()); EXPECT_FALSE(absent->has_value()); EXPECT_FALSE([manager fileExistsAtPath:missing.path]); EXPECT_EQ( - resolve_cache_directory(@"relative", StorageCreationMode::Create).error(), + resolve_test_cache_directory(@"relative", StorageCreationMode::Create) + .error(), Error::InvalidArgument); EXPECT_TRUE((!backup_excluded(parent) && !backup_excluded(sandbox.url))); } @@ -90,30 +92,33 @@ ASSERT_NE(storage.url, nil); NSURL* root = [storage.url URLByAppendingPathComponent:@"cache"]; ASSERT_TRUE( - resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); + resolve_test_cache_directory(root.path, StorageCreationMode::Create) + .ok()); StorageFaultScope fault(FaultPoint::BeforeCreateDirectory); EXPECT_TRUE( - resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); + resolve_test_cache_directory(root.path, StorageCreationMode::Create) + .ok()); EXPECT_TRUE( - resolve_cache_directory(root.path, StorageCreationMode::OpenExisting) + resolve_test_cache_directory(root.path, StorageCreationMode::OpenExisting) .ok()); EXPECT_EQ(fault.matches, 0); EXPECT_TRUE(backup_excluded(root)); NSURL* missing = [storage.url URLByAppendingPathComponent:@"missing"]; - auto absent = - resolve_cache_directory(missing.path, StorageCreationMode::OpenExisting); + auto absent = resolve_test_cache_directory( + missing.path, StorageCreationMode::OpenExisting); ASSERT_TRUE(absent.ok()); EXPECT_FALSE(absent->has_value()); EXPECT_EQ(fault.matches, 0); EXPECT_EQ( - resolve_cache_directory(missing.path, StorageCreationMode::Create) + resolve_test_cache_directory(missing.path, StorageCreationMode::Create) .error(), Error::AccessFailed); EXPECT_EQ(fault.hits, 1); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:missing.path]); EXPECT_TRUE( - resolve_cache_directory(missing.path, StorageCreationMode::Create).ok()); + resolve_test_cache_directory(missing.path, StorageCreationMode::Create) + .ok()); } TEST(CoreAICacheRootTest, CreatesDirectoriesWithDefaultPermissions) { @@ -125,7 +130,8 @@ ASSERT_EQ(stat(reference.fileSystemRepresentation, &expected), 0); NSURL* root = [storage.url URLByAppendingPathComponent:@"cache"]; ASSERT_TRUE( - resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); + resolve_test_cache_directory(root.path, StorageCreationMode::Create) + .ok()); FileDescriptor directory( open(root.fileSystemRepresentation, O_RDONLY | O_DIRECTORY | O_CLOEXEC)); ASSERT_GE(directory.get(), 0); @@ -144,10 +150,12 @@ FileDescriptor directory( ASSERT_NE(storage.url, nil); NSURL* root = [storage.url URLByAppendingPathComponent:@"cache"]; ASSERT_TRUE( - resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); + resolve_test_cache_directory(root.path, StorageCreationMode::Create) + .ok()); ASSERT_EQ(rmdir(root.fileSystemRepresentation), 0); ASSERT_TRUE( - resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); + resolve_test_cache_directory(root.path, StorageCreationMode::Create) + .ok()); EXPECT_TRUE(backup_excluded(root)); } @@ -156,7 +164,8 @@ FileDescriptor directory( ASSERT_NE(sandbox.url, nil); NSURL* root = [sandbox.url URLByAppendingPathComponent:@"cache"]; ASSERT_TRUE( - resolve_cache_directory(root.path, StorageCreationMode::Create).ok()); + resolve_test_cache_directory(root.path, StorageCreationMode::Create) + .ok()); FileDescriptor directory( open_directory_at(AT_FDCWD, root.fileSystemRepresentation)); ASSERT_GE(directory.get(), 0); @@ -201,7 +210,7 @@ FileDescriptor directory( ASSERT_TRUE([before[NSFileProtectionKey] isEqual:NSFileProtectionComplete]); for (StorageCreationMode mode : {StorageCreationMode::OpenExisting, StorageCreationMode::Create}) { - ASSERT_TRUE(resolve_cache_directory(root.path, mode).ok()); + ASSERT_TRUE(resolve_test_cache_directory(root.path, mode).ok()); NSDictionary* after = [manager attributesOfItemAtPath:root.path error:&error]; ASSERT_NE(after, nil) << error.description.UTF8String; @@ -222,8 +231,8 @@ FileDescriptor directory( first.fileSystemRepresentation, configured.fileSystemRepresentation), 0); - auto resolved = - resolve_cache_directory(configured.path, StorageCreationMode::Create); + auto resolved = resolve_test_cache_directory( + configured.path, StorageCreationMode::Create); ASSERT_TRUE(resolved.ok()); ASSERT_TRUE(resolved->has_value()); struct stat target_info; @@ -257,19 +266,21 @@ FileDescriptor directory( {StorageCreationMode::OpenExisting, StorageCreationMode::Create}) { SCOPED_TRACE(static_cast(mode)); EXPECT_EQ( - resolve_cache_directory(nil, mode).error(), Error::InvalidArgument); + resolve_test_cache_directory(nil, mode).error(), + Error::InvalidArgument); for (NSString* path in @[ @"", @"relative", @"~", @"~/cache", @"~nobody/cache" ]) { SCOPED_TRACE(path.UTF8String); EXPECT_EQ( - resolve_cache_directory(path, mode).error(), Error::InvalidArgument); + resolve_test_cache_directory(path, mode).error(), + Error::InvalidArgument); } for (NSString* path in @[ regular.path, [regular.path stringByAppendingPathComponent:@"child"] ]) { EXPECT_EQ( - resolve_cache_directory(path, mode).error(), + resolve_test_cache_directory(path, mode).error(), mode == StorageCreationMode::Create ? Error::InvalidArgument : Error::AccessFailed); } diff --git a/backends/apple/coreai/runtime/test/coreai_cache_test.mm b/backends/apple/coreai/runtime/test/coreai_cache_test.mm index 8af4292d21a..a04fa9e0e96 100644 --- a/backends/apple/coreai/runtime/test/coreai_cache_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_cache_test.mm @@ -17,8 +17,8 @@ #include #include #include -#include "coreai_bookmark_fixture.h" #include "coreai_cache.h" +#include "coreai_cache_entry_fixture.h" #include "coreai_fake_loader.h" #include "coreai_fault_scope.h" #include "coreai_filesystem_fixture.h" @@ -88,7 +88,7 @@ AssertionResult seed_cache( CacheEntry entry{ key.get(), nil, - [[root URLByAppendingPathComponent:@"staging"] + [[root URLByAppendingPathComponent:@"sources"] URLByAppendingPathComponent:key.get()]}; auto lock = acquire_test_cache_entry(root.path, entry.key); if (!lock.ok()) { @@ -449,7 +449,7 @@ FBSyntheticPTE pte( } } -TEST_F(CoreAICacheTest, ClearsStagingOnlyEntriesWithoutSdk) { +TEST_F(CoreAICacheTest, ClearsSourceOnlyEntriesWithoutSdk) { @autoreleasepool { TestDirectory root; BookmarkLoaderScope fake; @@ -465,7 +465,7 @@ FBSyntheticPTE pte( } } -TEST_F(CoreAICacheTest, MaintenanceUnlinksStagingRootWithoutFollowingIt) { +TEST_F(CoreAICacheTest, MaintenanceUnlinksSourcesRootWithoutFollowingIt) { @autoreleasepool { TestDirectory root, outside; BookmarkLoaderScope fake; @@ -531,7 +531,7 @@ FBSyntheticPTE valid( TEST_F( CoreAICacheTest, - InventoriesBookmarkAndStagingUnionPreservingUnownedFiles) { + InventoriesBookmarkAndSourceUnionPreservingUnownedFiles) { @autoreleasepool { TestDirectory root; BookmarkLoaderScope fake; @@ -547,7 +547,7 @@ FBSyntheticPTE valid( ASSERT_TRUE(seed_cache( root.url, cache_manifest('3'), fake.loader, stage_only, false, true)); NSURL* temporary = - [root.url URLByAppendingPathComponent:@"staging/.staging-unkeyed"]; + [root.url URLByAppendingPathComponent:@"sources/.pending-unkeyed"]; ASSERT_TRUE([NSFileManager.defaultManager createDirectoryAtURL:temporary withIntermediateDirectories:NO attributes:nil @@ -586,7 +586,7 @@ FBSyntheticPTE valid( } } -TEST_F(CoreAICacheTest, RemovesStagingOnlyEntriesWithoutSdkAvailability) { +TEST_F(CoreAICacheTest, RemovesSourceOnlyEntriesWithoutSdkAvailability) { @autoreleasepool { TestDirectory root; BookmarkLoaderScope fake; @@ -672,9 +672,10 @@ FBSyntheticPTE pte( logs.find("SDK eviction could not be confirmed"), std::string::npos); EXPECT_EQ( - logs.find(std::string( - static_cast(damaged.bookmark.bytes), - damaged.bookmark.length)), + logs.find( + std::string( + static_cast(damaged.bookmark.bytes), + damaged.bookmark.length)), std::string::npos); } cache_entry_removed(root.url, damaged); diff --git a/backends/apple/coreai/runtime/test/coreai_delegate_test.mm b/backends/apple/coreai/runtime/test/coreai_delegate_test.mm index afc117ae324..1321bed50f0 100644 --- a/backends/apple/coreai/runtime/test/coreai_delegate_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_delegate_test.mm @@ -15,8 +15,8 @@ #include #include -#include "coreai_bookmark_fixture.h" #include "coreai_cache.h" +#include "coreai_cache_entry_fixture.h" #import "coreai_delegate_fixture.h" #include "coreai_test_utils.h" @@ -176,7 +176,7 @@ void expect_preflight_rejection( ASSERT_TRUE(key.ok()); NSString* expected = [fixture.storage.url.path stringByAppendingPathComponent: - [NSString stringWithFormat:@"staging/%@/%@", + [NSString stringWithFormat:@"sources/%@/%@", key.get(), selected->path.lastPathComponent]]; EXPECT_TRUE([bridge.state().last_bundle isEqualToString:expected]); diff --git a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm index 44296beab6c..6a45af50a37 100644 --- a/backends/apple/coreai/runtime/test/coreai_eviction_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_eviction_test.mm @@ -11,7 +11,7 @@ #include #include #include "coreai_acquisition_fixture.h" -#include "coreai_bookmark_fixture.h" +#include "coreai_cache_entry_fixture.h" #include "coreai_fake_loader.h" #include "coreai_fault_scope.h" #include "coreai_load_coordinator.h" @@ -182,7 +182,7 @@ - (void)clearRetainedState { BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); TestData data; - auto selected = bookmark_bundle(); + auto selected = selected_bundle(manifest_dict()); ASSERT_TRUE(selected.ok()); auto key_result = test_cache_key(selected.get()); ASSERT_TRUE(key_result.ok()); @@ -247,7 +247,7 @@ - (void)clearRetainedState { BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); TestData data; - auto selected = bookmark_bundle(); + auto selected = selected_bundle(manifest_dict()); ASSERT_TRUE(selected.ok()); auto key_result = test_cache_key(selected.get()); ASSERT_TRUE(key_result.ok()); @@ -255,7 +255,8 @@ - (void)clearRetainedState { EXPECT_EQ( clear_test_cache_entry(root.url.path, key, fake.loader), Error::Ok); EXPECT_EQ(fake.loader->evictions.load(), 0); - // Retain staging after publication to exercise clearing's failure paths. + // Retain the source bundle after publication to exercise clearing's failure + // paths. { StorageFaultScope fault(FaultPoint::Remove); ASSERT_EQ( @@ -267,7 +268,7 @@ - (void)clearRetainedState { ASSERT_TRUE(saved_result.ok()); NSData* saved = saved_result.get(); ASSERT_NE(saved, nil); - NSURL* source_url = [[[root.url URLByAppendingPathComponent:@"staging"] + NSURL* source_url = [[[root.url URLByAppendingPathComponent:@"sources"] URLByAppendingPathComponent:key] URLByAppendingPathComponent:selected->path.lastPathComponent]; EXPECT_TRUE( @@ -335,7 +336,7 @@ - (void)clearRetainedState { TestDirectory root; ASSERT_NE(root.url, nil); TestData data; - auto selected = bookmark_bundle(); + auto selected = selected_bundle(manifest_dict()); ASSERT_TRUE(selected.ok()); auto key_result = test_cache_key(selected.get()); ASSERT_TRUE(key_result.ok()); @@ -362,7 +363,7 @@ - (void)clearRetainedState { ASSERT_TRUE(removed.ok()); EXPECT_EQ(removed.get(), nil); EXPECT_EQ(loader->evictions.load(), 2); - NSURL* source_url = [[[root.url URLByAppendingPathComponent:@"staging"] + NSURL* source_url = [[[root.url URLByAppendingPathComponent:@"sources"] URLByAppendingPathComponent:key] URLByAppendingPathComponent:selected->path.lastPathComponent]; EXPECT_FALSE( @@ -379,7 +380,7 @@ - (void)clearRetainedState { BookmarkLoaderScope fake; ASSERT_NE(fake.loader, nil); TestData data; - auto selected = bookmark_bundle(); + auto selected = selected_bundle(manifest_dict()); ASSERT_TRUE(selected.ok()); auto key_result = test_cache_key(selected.get()); ASSERT_TRUE(key_result.ok()); diff --git a/backends/apple/coreai/runtime/test/coreai_fake_loader.mm b/backends/apple/coreai/runtime/test/coreai_fake_loader.mm index ccfb8ee1780..5eefda12f9d 100644 --- a/backends/apple/coreai/runtime/test/coreai_fake_loader.mm +++ b/backends/apple/coreai/runtime/test/coreai_fake_loader.mm @@ -11,6 +11,7 @@ #include "coreai_test_utils.h" using executorch::backends::coreai::testing::FakeBridgeState; +using executorch::backends::coreai::testing::test_deadline; // ARC keeps failed asynchronous work from outliving its state storage. @interface CoreAIFakeBridgeContext : NSObject { @@ -83,10 +84,6 @@ void run_async(CoreAIFakeBridgeContext* context, void (^work)()) { } return ::testing::AssertionSuccess(); } - -dispatch_time_t cleanup_deadline() { - return executorch::backends::coreai::testing::test_deadline(); -} } // namespace @interface FakeOutputOwner : NSObject @@ -150,7 +147,7 @@ - (void)executeInputs:(NSArray*)inputs if (ready != nil) { dispatch_semaphore_signal(ready); if (proceed == nil || - dispatch_semaphore_wait(proceed, cleanup_deadline()) != 0) { + dispatch_semaphore_wait(proceed, test_deadline()) != 0) { ++context->state.input_wait_timeouts; completion(nil, fake_error(ETCoreAIErrorRuntime)); return; @@ -467,7 +464,7 @@ - (void)clearRetainedState { } ScopedFakeBridgeState::~ScopedFakeBridgeState() { - auto drained = wait_for_callbacks(cleanup_deadline()); + auto drained = wait_for_callbacks(test_deadline()); EXPECT_TRUE(drained); if (drained) { id loader = context_->state.bookmark_loader; @@ -516,7 +513,7 @@ - (void)clearRetainedState { if (context_ == nil) { return; } - auto drained = ::wait_for_callbacks(context_, cleanup_deadline()); + auto drained = ::wait_for_callbacks(context_, test_deadline()); EXPECT_TRUE(drained); if (drained) { [loader clearRetainedState]; diff --git a/backends/apple/coreai/runtime/test/coreai_filesystem_fixture.h b/backends/apple/coreai/runtime/test/coreai_filesystem_fixture.h index d15d865020c..7379eff4267 100644 --- a/backends/apple/coreai/runtime/test/coreai_filesystem_fixture.h +++ b/backends/apple/coreai/runtime/test/coreai_filesystem_fixture.h @@ -10,6 +10,9 @@ #import #include +#include +#include +#include "coreai_cache_root.h" namespace executorch::backends::coreai::testing { @@ -29,6 +32,19 @@ ::testing::AssertionResult asset_tree_snapshot( ::testing::AssertionResult snapshot_matches( NSURL* root, NSDictionary* expected); -NSArray* staging_directories(NSURL* root); +NSArray* pending_directories(NSURL* root); + +// Lets one test body cover both cache directory entry points. +inline runtime::Result> +resolve_test_cache_directory(NSString* path, StorageCreationMode mode) { + if (mode == StorageCreationMode::OpenExisting) { + return open_cache_directory(path); + } + auto created = create_cache_directory(path); + if (!created.ok()) { + return created.error(); + } + return std::optional(std::move(created.get())); +} } // namespace executorch::backends::coreai::testing diff --git a/backends/apple/coreai/runtime/test/coreai_filesystem_fixture.mm b/backends/apple/coreai/runtime/test/coreai_filesystem_fixture.mm index 827a228eb8e..1e5a7bdcb9a 100644 --- a/backends/apple/coreai/runtime/test/coreai_filesystem_fixture.mm +++ b/backends/apple/coreai/runtime/test/coreai_filesystem_fixture.mm @@ -137,15 +137,15 @@ << "Filesystem snapshot changed: " << root.path.UTF8String; } -NSArray* staging_directories(NSURL* root) { +NSArray* pending_directories(NSURL* root) { if (root == nil) { - ADD_FAILURE() << "Missing staging root"; + ADD_FAILURE() << "Missing cache root"; return nil; } - NSURL* cache_staging = [root URLByAppendingPathComponent:@"staging" + NSURL* cache_sources = [root URLByAppendingPathComponent:@"sources" isDirectory:YES]; - if ([NSFileManager.defaultManager fileExistsAtPath:cache_staging.path]) { - root = cache_staging; + if ([NSFileManager.defaultManager fileExistsAtPath:cache_sources.path]) { + root = cache_sources; } NSError* error = nil; NSArray* children = @@ -154,16 +154,16 @@ options:0 error:&error]; if (children == nil || error != nil) { - ADD_FAILURE() << "Cannot list staging directories: " + ADD_FAILURE() << "Cannot list pending directories: " << root.path.UTF8String; return nil; } - NSMutableArray* staging = [NSMutableArray array]; + NSMutableArray* pending = [NSMutableArray array]; for (NSURL* child in children) { - if ([child.lastPathComponent hasPrefix:@".staging-"]) - [staging addObject:child]; + if ([child.lastPathComponent hasPrefix:@".pending-"]) + [pending addObject:child]; } - return staging; + return pending; } } // namespace executorch::backends::coreai::testing diff --git a/backends/apple/coreai/runtime/test/coreai_host_test.mm b/backends/apple/coreai/runtime/test/coreai_host_test.mm index d87eefdbbba..3702c812e0d 100644 --- a/backends/apple/coreai/runtime/test/coreai_host_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_host_test.mm @@ -10,7 +10,7 @@ #include #include #include "coreai_acquisition_fixture.h" -#include "coreai_bookmark_fixture.h" +#include "coreai_cache_entry_fixture.h" int main(int argc, char** argv) { executorch::runtime::runtime_init(); diff --git a/backends/apple/coreai/runtime/test/coreai_source_fixture.h b/backends/apple/coreai/runtime/test/coreai_source_fixture.h index d0e51c257fb..a5377e4f93d 100644 --- a/backends/apple/coreai/runtime/test/coreai_source_fixture.h +++ b/backends/apple/coreai/runtime/test/coreai_source_fixture.h @@ -11,7 +11,7 @@ #include #include #include -#include "coreai_bookmark_fixture.h" +#include "coreai_cache_entry_fixture.h" #include "coreai_filesystem_fixture.h" #include "coreai_manifest_fixture.h" #include "coreai_source_bundle.h" @@ -39,9 +39,8 @@ class TestData final : public runtime::NamedDataMap { }; void aot_data(TestData& data, NSString* arch); -runtime::Result source_manifest(bool aot = false); NSString* source_key(const SelectedBundle& bundle); -NSURL* staging_root(NSURL* cache_root); +NSURL* sources_root(NSURL* cache_root); NSURL* source_entry(NSURL* cache_root, const SelectedBundle& bundle); class CoreAISourceTest : public ::testing::Test { diff --git a/backends/apple/coreai/runtime/test/coreai_source_fixture.mm b/backends/apple/coreai/runtime/test/coreai_source_fixture.mm index 93f29fb1cf3..c06cef50cf2 100644 --- a/backends/apple/coreai/runtime/test/coreai_source_fixture.mm +++ b/backends/apple/coreai/runtime/test/coreai_source_fixture.mm @@ -74,23 +74,19 @@ void aot_data(TestData& data, NSString* arch) { data.files[data.required_prefix + "nested/weights.bin"] = "weights"; } -Result source_manifest(bool aot) { - return selected_bundle(aot ? aot_manifest_dict() : manifest_dict()); -} - NSString* source_key(const SelectedBundle& bundle) { auto key = cache_variant_key(bundle, @"macOS", @"arch_b", @"default"); EXPECT_TRUE(key.ok()); return key.ok() ? key.get() : nil; } -NSURL* staging_root(NSURL* cache_root) { - return [cache_root URLByAppendingPathComponent:@"staging" isDirectory:YES]; +NSURL* sources_root(NSURL* cache_root) { + return [cache_root URLByAppendingPathComponent:@"sources" isDirectory:YES]; } NSURL* source_entry(NSURL* cache_root, const SelectedBundle& bundle) { return - [staging_root(cache_root) URLByAppendingPathComponent:source_key(bundle) + [sources_root(cache_root) URLByAppendingPathComponent:source_key(bundle) isDirectory:YES]; } diff --git a/backends/apple/coreai/runtime/test/coreai_source_integrity_test.mm b/backends/apple/coreai/runtime/test/coreai_source_integrity_test.mm index 33d75a503d3..264042988e7 100644 --- a/backends/apple/coreai/runtime/test/coreai_source_integrity_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_source_integrity_test.mm @@ -17,7 +17,7 @@ namespace executorch::backends::coreai::testing { using runtime::Error; using runtime::Result; -TEST_F(CoreAISourceTest, StagingRemainsAnchoredWhenConfiguredSymlinkChanges) { +TEST_F(CoreAISourceTest, SourcesRemainAnchoredWhenConfiguredSymlinkChanges) { TestDirectory storage; ASSERT_NE(storage.url, nil); NSURL* target = [storage.url URLByAppendingPathComponent:@"target"]; @@ -29,7 +29,7 @@ symlink( target.fileSystemRepresentation, configured.fileSystemRepresentation), 0); - auto bundle = source_manifest(); + auto bundle = selected_bundle(manifest_dict()); ASSERT_TRUE(bundle.ok()); auto key = test_cache_key(bundle.get()); ASSERT_TRUE(key.ok()); @@ -75,7 +75,8 @@ TestData data; if (aot) aot_data(data, @"arch_b"); - auto manifest = source_manifest(aot); + auto manifest = + selected_bundle(aot ? aot_manifest_dict() : manifest_dict()); ASSERT_TRUE(manifest.ok()); auto source = prepare_source_bundle(manifest.get(), &data, storage.url.path); @@ -124,7 +125,7 @@ EXPECT_EQ(data.attempts, attempts); EXPECT_TRUE(snapshot_matches(storage.url, snapshot)); EXPECT_TRUE(snapshot_matches(target.url, target_snapshot)); - EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_EQ(pending_directories(storage.url).count, 0u); } } } @@ -146,7 +147,7 @@ ASSERT_NE(storage.url, nil); ASSERT_NE(target.url, nil); TestData data; - auto bundle = source_manifest(); + auto bundle = selected_bundle(manifest_dict()); ASSERT_TRUE(bundle.ok()); auto source = prepare_source_bundle(bundle.get(), &data, storage.url.path); ASSERT_TRUE(source.ok()); @@ -206,7 +207,7 @@ isEqual:[NSData dataWithBytes:"model data" length:11]])); EXPECT_EQ(data.attempts, 2); EXPECT_EQ(data.requests, data.releases); - EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_EQ(pending_directories(storage.url).count, 0u); EXPECT_TRUE(snapshot_matches(target.url, target_snapshot)); } } @@ -219,7 +220,7 @@ struct stat expected; ASSERT_EQ(stat(reference.fileSystemRepresentation, &expected), 0); TestData data; - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); auto source = prepare_source_bundle(manifest.get(), &data, storage.url.path); ASSERT_TRUE(source.ok()); @@ -239,7 +240,7 @@ TestDirectory storage; ASSERT_NE(storage.url, nil); TestData data; - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); auto source = prepare_source_bundle(manifest.get(), &data, storage.url.path); ASSERT_TRUE(source.ok()); @@ -271,7 +272,7 @@ TestData data; data.fail_at = 1; data.failure_error = error; - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); EXPECT_EQ( prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), @@ -279,7 +280,7 @@ EXPECT_EQ(data.attempts, 1); EXPECT_EQ(data.requests, 0); EXPECT_EQ(data.releases, 0); - EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_EQ(pending_directories(storage.url).count, 0u); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_entry(storage.url, manifest.get()).path]); } @@ -290,14 +291,14 @@ ASSERT_NE(storage.url, nil); TestData data; data.files.clear(); - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); EXPECT_EQ( prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), Error::InvalidExternalData); EXPECT_EQ(data.attempts, 1); EXPECT_EQ(data.requests, 0); - EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_EQ(pending_directories(storage.url).count, 0u); } } // namespace executorch::backends::coreai::testing diff --git a/backends/apple/coreai/runtime/test/coreai_source_publication_test.mm b/backends/apple/coreai/runtime/test/coreai_source_publication_test.mm index 19623c90558..5179f875826 100644 --- a/backends/apple/coreai/runtime/test/coreai_source_publication_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_source_publication_test.mm @@ -24,7 +24,7 @@ ASSERT_NE(storage.url, nil); TestData data; data.files["coreai/ab/model.aimodel/graph.bin"] = std::string(size, 'x'); - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); EXPECT_EQ( prepare_source_bundle(manifest.get(), &data, storage.url.path).error(), @@ -32,7 +32,7 @@ EXPECT_EQ(data.attempts, 1); EXPECT_EQ(data.requests, 1); EXPECT_EQ(data.releases, 1); - EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_EQ(pending_directories(storage.url).count, 0u); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:source_entry(storage.url, manifest.get()).path]); } @@ -43,10 +43,10 @@ ASSERT_NE(storage.url, nil); TestData data; aot_data(data, @"arch_b"); - auto manifest = source_manifest(true); + auto manifest = selected_bundle(aot_manifest_dict()); ASSERT_TRUE(manifest.ok()); NSURL* final = source_entry(storage.url, manifest.get()); - NSURL* root = staging_root(storage.url); + NSURL* root = sources_root(storage.url); const NSUInteger file_count = manifest->files.count; __block int synced_files = 0; __block int full_syncs = 0; @@ -63,7 +63,7 @@ const bool published = [NSFileManager.defaultManager fileExistsAtPath:final.path]; ordered &= full_syncs == 1 ? !published : published; - ordered &= staging_directories(root).count == (full_syncs == 1 ? 1u : 0u); + ordered &= pending_directories(root).count == (full_syncs == 1 ? 1u : 0u); } if (operation == FaultPoint::Rename) { ++publications; @@ -79,10 +79,10 @@ EXPECT_EQ(data.requests, data.releases); } -TEST_F(CoreAISourceTest, PublicationFailuresCleanStagingWithoutAutomaticRetry) { +TEST_F(CoreAISourceTest, PublicationFailuresCleanPendingWithoutAutomaticRetry) { for (auto operation : - {FaultPoint::CreateStagingDirectory, - FaultPoint::OpenStagingDirectory, + {FaultPoint::CreatePendingDirectory, + FaultPoint::OpenPendingDirectory, FaultPoint::Write, FaultPoint::SyncFile, FaultPoint::SyncDirectory, @@ -92,7 +92,7 @@ TestDirectory storage; ASSERT_NE(storage.url, nil); TestData data; - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); NSString* key = source_key(manifest.get()); auto lock = acquire_test_cache_entry(storage.url.path, key); @@ -108,15 +108,15 @@ EXPECT_EQ(fault.matches, 1); EXPECT_EQ(fault.hits, 1); } - const int attempts = operation == FaultPoint::CreateStagingDirectory || - operation == FaultPoint::OpenStagingDirectory || + const int attempts = operation == FaultPoint::CreatePendingDirectory || + operation == FaultPoint::OpenPendingDirectory || operation == FaultPoint::SyncDirectory ? 0 : 1; EXPECT_EQ(data.attempts, attempts); EXPECT_EQ(data.requests, data.releases); EXPECT_FALSE([NSFileManager.defaultManager fileExistsAtPath:final.path]); - EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_EQ(pending_directories(storage.url).count, 0u); auto retried = executorch::backends::coreai::prepare_source_bundle( manifest.get(), &data, *lock.get()); ASSERT_TRUE(retried.ok()); @@ -124,17 +124,17 @@ } } -TEST_F(CoreAISourceTest, RetriesInterruptedStagingAndPublicationCalls) { +TEST_F(CoreAISourceTest, RetriesInterruptedPendingAndPublicationCalls) { for (auto operation : - {FaultPoint::CreateStagingDirectory, - FaultPoint::OpenStagingDirectory, + {FaultPoint::CreatePendingDirectory, + FaultPoint::OpenPendingDirectory, FaultPoint::SyncDirectory, FaultPoint::Rename}) { SCOPED_TRACE(static_cast(operation)); TestDirectory storage; ASSERT_NE(storage.url, nil); TestData data; - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); NSString* key = source_key(manifest.get()); auto lock = acquire_test_cache_entry(storage.url.path, key); @@ -148,7 +148,7 @@ EXPECT_GE(fault.matches, 2); EXPECT_EQ(data.attempts, 1); EXPECT_EQ(data.requests, data.releases); - EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_EQ(pending_directories(storage.url).count, 0u); } } @@ -156,7 +156,7 @@ TestDirectory storage; ASSERT_NE(storage.url, nil); TestData data; - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); NSURL* final = source_entry(storage.url, manifest.get()); { @@ -170,7 +170,7 @@ } EXPECT_EQ(data.attempts, 1); EXPECT_EQ(data.requests, data.releases); - EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_EQ(pending_directories(storage.url).count, 0u); NSURL* graph = [final URLByAppendingPathComponent:@"model.aimodel/graph.bin"]; EXPECT_TRUE(([[NSData dataWithContentsOfURL:graph] isEqual:[NSData dataWithBytes:"model data" length:11]])); @@ -203,7 +203,7 @@ ASSERT_NE(winner_storage.url, nil); TestData data; TestData winner_data; - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); auto winner = prepare_source_bundle( manifest.get(), &winner_data, winner_storage.url.path); @@ -214,7 +214,7 @@ NSURL* winner_entry = winner.get().URLByDeletingLastPathComponent; NSDictionary* snapshot = nil; ASSERT_TRUE(asset_tree_snapshot(winner_entry, snapshot)); - NSURL* root = staging_root(storage.url); + NSURL* root = sources_root(storage.url); NSURL* final = source_entry(storage.url, manifest.get()); __block int publications = 0; __block bool callback_ok = true; @@ -225,7 +225,7 @@ if (operation != FaultPoint::Rename) return; ++publications; - callback_ok &= staging_directories(root).count == 1; + callback_ok &= pending_directories(root).count == 1; callback_ok &= [NSFileManager.defaultManager moveItemAtURL:winner_entry toURL:final error:nil]; @@ -238,20 +238,20 @@ } EXPECT_EQ(data.attempts, 1); EXPECT_EQ(data.requests, data.releases); - EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_EQ(pending_directories(storage.url).count, 0u); EXPECT_TRUE(snapshot_matches(final, snapshot)); } TEST_F( CoreAISourceTest, - ExclusivePublicationValidatesWinnerAndRemovesLosingStaging) { + ExclusivePublicationValidatesWinnerAndRemovesLosingPending) { TestDirectory storage; TestDirectory winner_storage; ASSERT_NE(storage.url, nil); ASSERT_NE(winner_storage.url, nil); TestData data; TestData winner_data; - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); auto winner = prepare_source_bundle( manifest.get(), &winner_data, winner_storage.url.path); @@ -259,7 +259,7 @@ NSURL* winner_entry = winner.get().URLByDeletingLastPathComponent; NSDictionary* snapshot = nil; ASSERT_TRUE(asset_tree_snapshot(winner_entry, snapshot)); - NSURL* root = staging_root(storage.url); + NSURL* root = sources_root(storage.url); NSURL* final = source_entry(storage.url, manifest.get()); __block int publications = 0; __block bool callback_ok = true; @@ -271,7 +271,7 @@ if (operation != FaultPoint::Rename) return; ++publications; - callback_ok &= staging_directories(root).count == 1; + callback_ok &= pending_directories(root).count == 1; callback_ok &= [NSFileManager.defaultManager moveItemAtURL:winner_entry toURL:final error:nil]; @@ -288,7 +288,7 @@ } EXPECT_EQ(data.attempts, manifest->files.count); EXPECT_EQ(data.requests, data.releases); - EXPECT_EQ(staging_directories(storage.url).count, 0u); + EXPECT_EQ(pending_directories(storage.url).count, 0u); EXPECT_TRUE(snapshot_matches(final, snapshot)); } diff --git a/backends/apple/coreai/runtime/test/coreai_source_test.mm b/backends/apple/coreai/runtime/test/coreai_source_test.mm index 90bb568337a..67ec531c6a0 100644 --- a/backends/apple/coreai/runtime/test/coreai_source_test.mm +++ b/backends/apple/coreai/runtime/test/coreai_source_test.mm @@ -140,7 +140,7 @@ TestDirectory storage; ASSERT_NE(storage.url, nil); TestData data; - auto selected = source_manifest(); + auto selected = selected_bundle(manifest_dict()); ASSERT_TRUE(selected.ok()); for (int pass = 0; pass < 2; ++pass) { SCOPED_TRACE(pass == 0 ? "cold" : "warm"); @@ -207,7 +207,7 @@ EXPECT_EQ(data.metadata_requests, 0); EXPECT_EQ( [NSFileManager.defaultManager - contentsOfDirectoryAtPath:staging_root(storage.url).path + contentsOfDirectoryAtPath:sources_root(storage.url).path error:nil] .count, 1u); @@ -219,7 +219,7 @@ } TEST_F(CoreAISourceTest, MissingSourceRequiresNamedData) { - auto bundle = source_manifest(); + auto bundle = selected_bundle(manifest_dict()); ASSERT_TRUE(bundle.ok()); TestData data; TestDirectory storage; @@ -242,7 +242,7 @@ TestData data; TestDirectory storage; ASSERT_NE(storage.url, nil); - auto manifest = source_manifest(); + auto manifest = selected_bundle(manifest_dict()); ASSERT_TRUE(manifest.ok()); NSURL* first_url = nil; { @@ -306,7 +306,7 @@ EXPECT_EQ(data.releases, data.requests); } -TEST_F(CoreAISourceTest, PartialMaterializationCleansStagingAndCanRetry) { +TEST_F(CoreAISourceTest, PartialMaterializationCleansPendingAndCanRetry) { TestDirectory storage; ASSERT_NE(storage.url, nil); TestData data; @@ -326,7 +326,7 @@ EXPECT_EQ(data.attempts, 2); EXPECT_EQ( [NSFileManager.defaultManager - contentsOfDirectoryAtPath:staging_root(storage.url).path + contentsOfDirectoryAtPath:sources_root(storage.url).path error:nil] .count, 0u); @@ -336,7 +336,7 @@ prepare_source_bundle(manifest.get(), &data, storage.url.path).ok()); EXPECT_EQ( [NSFileManager.defaultManager - contentsOfDirectoryAtPath:staging_root(storage.url).path + contentsOfDirectoryAtPath:sources_root(storage.url).path error:nil] .count, 1u); @@ -390,11 +390,11 @@ TEST_F( CoreAISourceTest, - ExplicitStagingKeyReusesSourceWithoutWritesOrRepublication) { + ExplicitSourceKeyReusesSourceWithoutWritesOrRepublication) { TestDirectory directory; ASSERT_NE(directory.url, nil); TestData data; - auto bundle = source_manifest(); + auto bundle = selected_bundle(manifest_dict()); ASSERT_TRUE(bundle.ok()); auto key = cache_variant_key( bundle.get(), @"macOS", @"arch_b", @"explicit-test-options"); @@ -407,7 +407,7 @@ EXPECT_EQ(data.attempts, 1); NSString* expected_path = [directory.url.URLByResolvingSymlinksInPath.path stringByAppendingPathComponent: - [NSString stringWithFormat:@"staging/%@/model.aimodel", key.get()]]; + [NSString stringWithFormat:@"sources/%@/model.aimodel", key.get()]]; EXPECT_TRUE([first.get().path isEqualToString:expected_path]); NSDictionary* snapshot = nil; ASSERT_TRUE(asset_tree_snapshot(directory.url, snapshot));