Skip to content

Conversation

danbenner-vega
Copy link
Contributor

ObservationImageRepository upgrade

  • previous usage include @injected...
  • now it's a Singleton and uses Async Await

Profiled

  • before and after doesn't actually show much difference, which is good
  • but now we have safe threading
Before After

Copy link
Contributor

@paulsolt-ofsw paulsolt-ofsw left a comment

Choose a reason for hiding this comment

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

Looking good. I haven't tested it yet, but I left feedback. See if any of the comments make sense, and we can regroup on Monday.

Comment on lines 24 to +26
class ObservationImageRepositoryImpl: ObservationImageRepository, ObservableObject {

static let shared = ObservationImageRepositoryImpl()
Copy link
Contributor

Choose a reason for hiding this comment

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

Feels like this should be actor, or concurrency protections are needed a level up.

Isn't this the crux of the problem if we're modifying shared state across multiple threads? (See my next comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TLDR, making the shared an actor would make the entire thing and all of it's parts an actor. We don't need or want this. We only care about protecting what needs protecting.

Comment on lines +41 to +44
actor ImageCache {
private var cache = NSCache<NSString, UIImage>()

init(limit: Int = 100) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to use actor for ImageCache, instead of the ObservationImageRepositoryImpl?

NSCache is already thread safe, so I'm not sure how this helps to wrap it again.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we do not want the entire repo to be an actor, we choose to make only what is required be an actor. In this case it's the cache itself. It doesn't matter that NSCache is already safe, our ImageCache is not just an NSCache. Besides, this was the entire point, to very clearly identify and manage the cache as an actor

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm trying to reason about the change.

Is the point that this actor is enforcing read/write access using the actor mechanism, while the previous was technical thread safe, it wasn't enforcing proper shared access across all CRUD type operations?

Copy link
Contributor

Choose a reason for hiding this comment

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

I still question if actor is at the right level. It feels like it's not providing much benefit, when there is shared state in the repository that is unprotected.

Comment on lines +161 to +162
@MainActor
func imageAtPath(imagePath: String?) async -> UIImage {
Copy link
Contributor

Choose a reason for hiding this comment

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

My only concern is that we're doing background type work on the main thread. If this is truly loading files, it feels like the asynchronous work needs to happen on a background thread.

  • I'm having trouble understanding the current Swift 5.10 model vs. 6.2 with upcoming build settings.
  • If the image is cached, loading is fast, but if it's not, we need to do a more expensive load option (small files might not be an issue ... is this tile maps or just icons?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This work related to UI. It may be related to files, but it's directly related to updating the UI.

Copy link
Contributor

Choose a reason for hiding this comment

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

Doing the image file loading on the main thread is going to be problematic. That should be in the background.

Comment on lines +94 to +96
guard let path = iconPath else { return }
let image = await ObservationImageRepositoryImpl.shared.imageAtPath(imagePath: path)
uiImage = image
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we inject the shared instance like you do below?

var imageRepository: ObservationImageRepository

    init(imageRepository: ObservationImageRepository = ObservationImageRepositoryImpl.shared /* Other parameters */) {
    // ...
    }

Comment on lines 202 to 208
if let image = UIImage(contentsOfFile: resolvedPath), let cgImage = image.cgImage {
let scale = image.size.width / annotationScaleWidth
let scaledImage = UIImage(cgImage: cgImage, scale: scale, orientation: image.imageOrientation)
imageCache.setObject(scaledImage, forKey: cacheKey)
await cache.set(scaledImage, for: cacheKey)
scaledImage.accessibilityIdentifier = resolvedPath
return scaledImage
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Here we're going to do I/O on the main thread, so I think we'd need to bump it to the background in a fashion like this:

// I/O and resizing are expensive operations
 let loadedImage: UIImage? = await Task.detached {
        guard let image = UIImage(contentsOfFile: resolvedPath),
              let cgImage = image.cgImage else {
            return nil
        }
        
        let scale = image.size.width / annotationScaleWidth
        return UIImage(cgImage: cgImage, scale: scale, orientation: image.imageOrientation)
}.value // Await the result of the background work

Copy link
Contributor

@paulsolt-ofsw paulsolt-ofsw left a comment

Choose a reason for hiding this comment

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

I think this is on the right track for the @Injected replacement, but I think we need to rework the Swift Concurrency logic more to make this safe (and non-blocking). The image loading is going to block the main thread as it is written.

I'm still wrestling with how we should be adopting Swift 6.2 and better concurrency features. Those warnings, if enabled might help us focus on key problems at compile time (whack-a-mole).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants