Strategy 2 (iOS): Caching Offer Images to Improve Performance by Alonso Hernandez - ISIS3510-MOBILE-T34/T34-Wiki-SpendiQ GitHub Wiki

Strategy Description

This strategy focuses on the scenario where a user navigates to the "Special Offers" section to view detailed information about the offers, including the images of the shops they belong to. The objective is reducing the computational workload involved in displaying images for offers within this section. Previously, the app retrieved the shop image for each offer from its URL every time the section was loaded, requiring the CPU and GPU to handle the full data of the image and render it repeatedly. To address this, an image caching mechanism was introduced to store the images locally, allowing the app to retrieve them from cache rather than downloading them again. This optimization minimizes the load on both the CPU and GPU, thereby improving the app's responsiveness and reducing power consumption.


Screenshot 1: Special Offers Section

Special Offers Section

The "Special Offers" section shows details of nearby offers. On the right side of each offer's description, an image of the shop is displayed. This image is retrieved dynamically from a web URL and rendered in the app, which demands considerable CPU and GPU resources, especially when multiple offers are displayed simultaneously.


Optimization Strategy Implementation

The optimization was implemented by introducing an ImageCacheManager class, which serves as a centralized system for managing image caching locally. This class includes functionality to save images to cache, retrieve them by a unique key, and clear outdated or unused entries. The OfferViewModel was updated to integrate this caching logic, ensuring that images are fetched from cache when available and only downloaded when necessary. This approach significantly reduces redundant processing and network usage, while streamlining the user experience.

ImageCacheManager

class ImageCacheManager {
    static let shared = ImageCacheManager() // Singleton instance
    private let cacheDirectory: URL

    private init() {
        let paths = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
        cacheDirectory = paths[0].appendingPathComponent("OfferImages")
        
        if !FileManager.default.fileExists(atPath: cacheDirectory.path) {
            try? FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true, attributes: nil)
        }
    }

    func saveImage(_ image: UIImage, forKey key: String) {
        let fileURL = cacheDirectory.appendingPathComponent(key)
        guard let data = image.jpegData(compressionQuality: 0.8) else { return }
        try? data.write(to: fileURL)
    }

    func loadImage(forKey key: String) -> UIImage? {
        let fileURL = cacheDirectory.appendingPathComponent(key)
        guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }
        return UIImage(contentsOfFile: fileURL.path)
    }

    func clearCache(forKeys keys: [String]) {
        keys.forEach { key in
            let fileURL = cacheDirectory.appendingPathComponent(key)
            try? FileManager.default.removeItem(at: fileURL)
        }
    }
}

The ImageCacheManager provides essential methods for managing image storage locally. It ensures that images are compressed before being saved, minimizing storage space usage. The loadImage method retrieves images efficiently, reducing the need for redundant network calls. The clearCache function removes outdated images, keeping the cache clean and optimized.

Integration in OfferViewModel

func removeOldOfferImages(newOffers: [Offer]) {
    let newOfferKeys = Set(newOffers.compactMap { $0.id })
    let currentOfferKeys = Set(offers.compactMap { $0.id })
    let removedOffers = currentOfferKeys.subtracting(newOfferKeys)
    ImageCacheManager.shared.clearCache(forKeys: Array(removedOffers))
}

func fetchOffers() {
    db.collection("offers").getDocuments { [weak self] snapshot, error in
        guard let self = self else { return }
        var fetchedOffers: [Offer] = []
        
        for doc in snapshot?.documents ?? [] {
            let data = doc.data()
            guard let id = doc.documentID,
                  let shopImageURL = data["shopImage"] as? String else { continue }
            
            // Fetch or load the image
            let cachedImage = ImageCacheManager.shared.loadImage(forKey: id)
            if cachedImage == nil {
                // Download image and save to cache
                URLSession.shared.dataTask(with: URL(string: shopImageURL)!) { data, _, _ in
                    if let data = data, let image = UIImage(data: data) {
                        ImageCacheManager.shared.saveImage(image, forKey: id)
                    }
                }.resume()
            }

            // Append offer
            fetchedOffers.append(Offer(id: id, shopImage: shopImageURL))
        }

        DispatchQueue.main.async {
            self.removeOldOfferImages(newOffers: fetchedOffers)
            self.offers = fetchedOffers
        }
    }
}

The fetchOffers method integrates the caching logic. It first checks if an image exists in the cache. If not, the image is downloaded, stored in the cache, and then used for rendering. The removeOldOfferImages function ensures that only images relevant to the current set of offers are retained, preventing the cache from becoming bloated with outdated data.


Performance Evaluation

In this evaluation, the focus is on analyzing the app's performance when a user navigates to the "Special Offers" section, where offer details, including shop images, are dynamically loaded and displayed. The primary metric used for this analysis is CPU usage, specifically the percentage of CPU utilization over time, measured using the CPU Profiler in Xcode Developer Tools. This metric is critical as it reflects the computational workload required to handle operations such as image retrieval, processing, and rendering. High CPU usage can result in slower app responsiveness, increased power consumption, and overheating of user devices, all of which negatively impact the user experience. By evaluating this metric, we aim to measure the effectiveness of the image caching optimization in reducing the computational load and improving the overall efficiency of the app.

Screenshot 2: CPU Profiler Before Optimization

CPU Profiler Before Optimization

The CPU Profiler graph shows the computational workload during the time a user opens the "Special Offers" section. Before optimization, a peak of **243.0% CPU usage over 10 ms** is observed. This reflects the heavy processing required to dynamically download and render shop images for all offers in the section, significantly impacting app responsiveness and battery life.

Screenshot 3: CPU Profiler After Optimization

CPU Profiler After Optimization

After the optimization, the CPU Profiler indicates a reduced peak of **176.5% CPU usage over 10 ms** when opening the same section. The caching mechanism reduces the need for redundant image downloads and rendering, leading to a significant decrease in computational overhead.

Explanation of CPU Profiler and Performance Impact

The CPU Profiler measures the percentage of CPU resources utilized by the app over a specific period. Peaks in CPU usage, such as those observed during the "Special Offers" section load, indicate intensive processing tasks. Reducing these peaks improves the app's scalability and responsiveness, lowers energy consumption, and minimizes device overheating. This optimization makes the app more efficient and enhances the overall user experience.


Conclusions

The image caching mechanism significantly improved the app's performance, particularly during the "Special Offers" section load. By locally storing shop images, the app reduced its reliance on network calls and minimized the computational workload for image rendering. The CPU usage peak was reduced from 243.0% to 176.5%, a 27.3% decrease in CPU load. This reduction ensures smoother performance, faster load times, and improved energy efficiency, benefiting both the app's scalability and the user experience.

⚠️ **GitHub.com Fallback** ⚠️