8

I'm experiencing a performance hit when preloading SKTextureAtlas:

let textureAtlas = SKTextureAtlas(named: atlasName)
textureAtlas.preload(completionHandler: {
    ...
})

By performance hit, I mean FPS dropping to ~50 for a short amounts of time.

I tested it with Time Profiler in Instruments and verified that this work is indeed being done on a worker thread, like stated in documentation.

The image bellow shows a Time Profiler capture of the spike, caused by preloading atlas. As you can see, most of the spike is caused by 2 worker threads, which all seem to be loading image data, as far as I can understand. However, this should not cause a performance hit on the main thread IMHO.

enter image description here

Note 1: The .spriteatlas I'm preloading is not that big: 4 assets with approx. 1000x1000 size.

Note 2: I'm testing with iPhone 6, iOS 10, Xcode 8.

Note 3: There is no other substantial work being done at the same time; CPU is hovering at ~30% all the time. Same goes for GPU.

Note 4: The atlas preload is requested way before any of the textures from that atlas is needed, so it should have more than enough time to preload.

Any help/direction greatly appreciated!


UPDATE

Complete code block where the preload happens:

let updateGroup = DispatchGroup()

for assetDefinition in assetContainmentDefinitions {

    let assetName = assetDefinition.assetName

    // Check if asset is not needed anymore and clear the cache with it
    if progress >= assetDefinition.range.to {
        if cachedAssets[assetName] != nil {
            cachedAssets[assetName] = nil
        }
    }
    // Check if asset is needed and if it's not already loading then preload and cache it
    else if progress >= assetDefinition.range.from {

        if currentlyLoadingAssets.contains(assetName) == false &&
            cachedAssets[assetName] == nil {

            currentlyLoadingAssets.append(assetName)

            // Enter dispatch group
            updateGroup.enter()

            switch assetDefinition.assetType {
            case .textureAtlas:

                let textureAtlas = SKTextureAtlas(named: assetName)
                textureAtlas.preload(completionHandler: {

                    DispatchQueue.main.async { [weak self] in

                        self?.cachedAssets[assetName] = textureAtlas
                        self?.currentlyLoadingAssets.remove(object: assetName)

                        // Leave dispatch group after preload is done
                        updateGroup.leave()
                    }
                })

            case .texture:

                let texture = SKTexture(imageNamed: assetName)
                texture.preload(completionHandler: {

                    DispatchQueue.main.async { [weak self] in

                        self?.cachedAssets[assetName] = texture
                        self?.currentlyLoadingAssets.remove(object: assetName)

                        // Leave dispatch group after preload is done
                        updateGroup.leave()
                    }
                })
            }
        }
    }
}

// Call completion after the dispatch group is fully completed
if let completion = completion {
    updateGroup.notify(queue: DispatchQueue.main, execute: completion)
}

UPDATE 2

I've created an empty project with nothing but atlas preload block. The performance drop still occurs. I've tried with multiple atlases, even with atlas with only one asset.

I've also tried what @Sez suggested (see bellow), in this empty new project, but in that case the completion block didn't even get called, which seems like another bug in SKTextureAtlas class. (?!)

let atlas = SKTextureAtlas(dictionary: ["texture1": UIImage(named: "texture1")!])

atlas.preload(completionHandler: { [weak self] in
    print("COMPLETION BLOCK NOT CALLED")
    self?.cachedAtlas = atlas
})

UPDATE 3

I tried deleting the .spriteatlas and creating the .atlas with the same textures and there is (almost) no performance hit. However, .atlas doesn't support slicing which is why I want to use .spriteatlas in the first place.

damirstuhec
  • 6,069
  • 1
  • 22
  • 39
  • I do not think it is your atlases that are the problem, you have something else going on, code will be needed – Knight0fDragon Nov 27 '16 at 23:26
  • @Knight0fDragon please see update above. There really isn't anything much to it. I call this block from a main thread, wait for a completion to be fired and that's it. – damirstuhec Nov 27 '16 at 23:34
  • you need to diagnose mutliple things. First, start a new project with literally nothing else going on... Then, try running your thread and see if performance drops. Then, take out the multithreading call in your main project and see if the performance drops. Then, make the multithreading call, but with no actions (remove the atlases). (see second comment due to length) – Fluidity Nov 30 '16 at 01:53
  • We don't see anything particularly damaging in your function above. The only other idea I have is to overload all of the built-in steps in SK cycle and check delta-time for each (to see which one is lagging / taking the longest) https://developer.apple.com/library/content/documentation/GraphicsAnimation/Conceptual/SpriteKit_PG/Actions/Actions.html – Fluidity Nov 30 '16 at 01:53
  • If you can run those 3 tests, or post more of your code, then we can maybe have enough info to give a definitive answer. Hope this helps! – Fluidity Nov 30 '16 at 01:54
  • @Fluidity I created a new, empty SpriteKit project with just `atlas.preload(completionHandler:)` block and I still experience the same issue. Tried with multiple different atlases. – damirstuhec Dec 01 '16 at 01:23
  • Ouch. This is looking like a bug. Perhaps a workaround would be to just use textures *eep*... or you could create functions that create atlases on the fly from a list of textures, instead of trying to load premade atlases. I would hope that there would be a better workaround for this. Sometimes typecasting works, or using specifics.. I'm trying to think if any of this could apply here... – Fluidity Dec 01 '16 at 04:14
  • spitballing, what if you wrapped all of this up into a function, then threaded the function, with the atlas load as a third thread? Or does your main-thread NEED this info right away? Also, aren't there other types/patterns of threading? Perhaps one of those could work. – Fluidity Dec 01 '16 at 04:18
  • @Fluidity See my update 2, where I explain that creating texture atlas on the fly did not work. Can you be more exact with your last comment? I already tried putting everything on a background queue and it doesn't help. – damirstuhec Dec 01 '16 at 09:57
  • @Fluidity See my update 3. – damirstuhec Dec 01 '16 at 10:02

1 Answers1

6

There are issues plaguing SKTextureAtlas (Apple Dev forums post).

Try creating your texture atlas using the SKTextureAtlas:withDictionary: supplying a dictionary of [String:UIImage], a method described on another StackOverflow post and see if that helps.

UPDATE: If you want that preload of the SKTexture to happen in a background thread, I'd write that specifically rather than relying on the preload convenience function.

let atlas: SKTextureAtlas
let myImages: [String: UIImage] = getImages()
let globalQueue = DispatchQueue.global()
globalQueue.async {
    let atlas = SKTextureAtlas(withDictionary:myImages)
    for (k,v) in myImages {
        let tex = atlas.textureNamed(k)
        print(tex.size()) // force load
    }
    DispatchQueue.main.async {
        completionHandler(atlas)
    }
}

Regarding app slicing, as long as you place assets in Asset catalogs they should be sliced. After you upload a build to iTunesConnect you can see a report of the App Store sizes reflecting this.

iTunesConnect app store sizes

I have a video I recorded of a live stream where I show this import process. Sorry its a bit agonisingly long (2hrs+), but the link here goes right to the moment where the import into the sprite sheet happens.

https://youtu.be/Ic9Wnux8vd8?t=1h17m

Doing it the way I show here you get a sprite atlas, with sliced images. My scripts (which I demonstrate earlier in the clip) are on my GitHub if you want them.

Community
  • 1
  • 1
Sez
  • 1,275
  • 11
  • 26
  • Seriously, Apple still hasn't sorted out sprite textures... for something called SPRITEkit? – Confused Dec 04 '16 at 03:25
  • 1
    You should see the mess that On-Demand Resources is in! Sprite textures are a dream by comparison. :-) – Sez Dec 06 '16 at 02:51