1

I’m out of ideas here, SceneKit is piling on the memory and I’m only getting started. I’m displaying SNCNodes which are stored in arrays so I can separate components of the molecule for animation. These trees model molecules of which I will ultimately have maybe 50 to display, say one per “chapter”. The issue is when I move to another chapter the molecules from previous chapters persist in memory.

The molecule nodes are trees of child nodes. About half of the nodes are empty containers for orientation purposes. Otherwise, the geometries are SCNPrimitives (spheres, capsules, and cylinders). Each geometry has a specular and a diffuse material consisting of a UIColor, no textures are used.

When the app first boots, these molecules are constructed from code and archived into a dictionary. Then, and on subsequent boots, the archived dictionary is read into a local dictionary for use by the VC. (I’m removing safety features in this post for brevity.)

moleculeDictionary = Molecules.readFile() as! [String: [SCNNode]]

When a chapter wants to display a molecule it calls a particular function that loads the needed components for a given molecule from the local dictionary into local SCNNode properties.

// node stores (reuseable)
var atomsNode_1 = SCNNode() 
var atomsNode_2 = SCNNode()
        . . .

func lysozyme() {   // called by a chapter to display this molecule 
        . . .
    components = moleculeDictionary["lysozyme"]

    atomsNode_1 = components[0]         // protein w/CPK color
    baseNode.addChildNode(atomsNode_1)
    atomsNode_2 = components[2]         // NAG
    baseNode.addChildNode(atomsNode_2)
        . . .
 }

Before the next molecule is to be displayed, I call a “clean up” function:

atomsNode_1.removeFromParentNode()
atomsNode_2.removeFromParentNode()
        . . .

When I investigate in instruments, most of the bloated memory is 32 kB chunks called by C3DMeshCreateFromProfile and 80 kB chunks of C3DMeshCreateCopyWithInterleavedSources.

I also have leaks I need to trace which are traceable to the NSKeyedUnarchiver decoding of the archive. So I need to deal with these as well but they are a fraction of the memory use that’s accumulating each molecule call.

If I return to a previously viewed molecule, there is no further increase in memory usage, it all accumulates and persists.

I’ve tried declaring atomsNode_1 and its kin as optionals then setting them to nil at clean up time. No help. I’ve tried, in the clean up function,

atomsNode_1.enumerateChildNodesUsingBlock({
    node, stop in
    node.removeFromParentNode()
})

Well, the memory goes back down but the nodes seem to now be permanently gone from the loaded dictionary. Damn reference types!

So maybe I need a way to archive the [SCNNode] arrays in such a way as to unarchive and retrieve them individually. In this scenario I would clear them out of memory when done and reload from the archive when revisiting that molecule. But I know not yet how to do either of these. I’d appreciate comments about this before investing more time to be frustrated.

bpedit
  • 1,176
  • 8
  • 22

2 Answers2

4

Spheres, capsules, and cylinders all have fairly dense meshes. Do you need all that detail? Try reducing the various segment count properties (segmentCount, radialSegmentCount, etc). As a quick test, substitute SCNPyramid for all of your primitive types (that's the primitive with the lowest vector count). You should see a dramatic reduction in memory use if this is a factor (it will look ugly, but will give you immediate feedback on whether you're on a usable track). Can you use a long SCNBox instead of a cylinder?

Another optimization step would be to use SCNLevelOfDetail to allow substitute, low vertex count geometry when an object is far away. That would be more work than simply reducing the segment counts uniformly, but would pay off if you sometimes need greater detail.

Instead of managing the components yourself in arrays, use the node hierarchy to do that. Create each molecule, or animatable piece of a molecule, as a tree of SCNNodes. Give it a name. Make a flattenedClone. Now archive that. Read the node tree from archive when you need it; don't worry about arrays of nodes.

Consider writing two programs. One is your iOS program that manipulates/displays the molecules. The other is a Mac (or iOS?) program that generates your molecule node trees and archives them. That will give you a bunch of SCNNode tree archives that you can embed, as resources, in your display program, with no on-the-fly generation.

An answer to scene kit memory management using swift notes the need to nil out "textures" (materials or firstMaterial properties?) to release the node. Seems worth a look, although since you're just using UIColor I doubt it's a factor.

Here's an example of creating a compound node and archiving it. In real code you'd separate the archiving from the creation. Note also the use of a long skinny box to simulate a line. Try a chamfer radius of 0!

extension SCNNode {

public class func gizmoNode(axisLength: CGFloat) -> SCNNode {
    let offset = CGFloat(axisLength/2.0)
    let axisSide = CGFloat(0.1)
    let chamferRadius = CGFloat(axisSide)

    let xBox = SCNBox(width: axisLength, height: axisSide, length: axisSide, chamferRadius: chamferRadius)
    xBox.firstMaterial?.diffuse.contents = NSColor.redColor()
    let yBox = SCNBox(width: axisSide, height: axisLength, length: axisSide, chamferRadius: chamferRadius)
    yBox.firstMaterial?.diffuse.contents = NSColor.greenColor()
    let zBox = SCNBox(width: axisSide, height: axisSide, length: axisLength, chamferRadius: chamferRadius)
    zBox.firstMaterial?.diffuse.contents = NSColor.blueColor()
    let xNode = SCNNode(geometry: xBox)
    xNode.name = "X axis"
    let yNode = SCNNode(geometry: yBox)
    yNode.name = "Y axis"
    let zNode = SCNNode(geometry: zBox)
    zNode.name = "Z axis"

    let result = SCNNode()
    result.name = "Gizmo"
    result.addChildNode(xNode)
    result.addChildNode(yNode)
    result.addChildNode(zNode)
    xNode.position.x = offset
    yNode.position.y = offset
    zNode.position.z = offset

    let data = NSKeyedArchiver.archivedDataWithRootObject(result)
    let filename = "gizmo"

    // Save data to file
    let DocumentDirURL = try! NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true)

    // made the extension "plist" so you can easily inspect it by opening in Finder. Could just as well be "scn" or "node"
    // ".scn" can be opened in the Xcode Scene Editor
    let fileURL = DocumentDirURL.URLByAppendingPathComponent(filename).URLByAppendingPathExtension("plist")
    print("FilePath:", fileURL.path)

    if (!data.writeToURL(fileURL, atomically: true)) {
        print("oops")
    }
    return result
}
}   
Community
  • 1
  • 1
Hal Mueller
  • 7,019
  • 2
  • 24
  • 42
  • Hal, thanks for the feedback. Boxes won't cut it, it would look pretty unorthodox for a molecular display. On some models I could probably reduce the poly count but with as many models as I'll have that won't be very efficatious in the lon run. I really need to prevent models from accumulating. I think reading them one at a time is the potential solution but so far I've only managed to save them all together in one dictionary. There are animation reasons the nodes are in arrays plus : it takes no more memory (I've tested) plus waaay faster animation than enumeration. – bpedit Feb 29 '16 at 01:16
  • So, I really like your idea of "a bunch of node trees embedded as resources...". In fact, I've posted 2 1/2 questions here on how to do exactly that but have received no responses! Suggestions welcome! Thanks again, Byrne – bpedit Feb 29 '16 at 01:26
  • Edited with some sample code. You might be tickling a SceneKit memory cycle; http://stackoverflow.com/questions/32997711/removing-scnnode-does-not-free-memory-before-creating-new-scnnode?rq=1 may be the same thing. As a workaround, you could try destroying/recreating the SCNScene, or the SCNView. – Hal Mueller Feb 29 '16 at 01:50
  • Thanks a bunch. It may take a couple days to get to this. Already I see I've limited myself in thinking I had so save all graphs to one file. I also will test wiping the scene or view. Also looking at saving as .scn instead of nodes. – bpedit Feb 29 '16 at 04:26
  • Still working on the memory issue. BUT: you've solved my issue of archiving! Please copy the last part of your code above to the linked post below so I can mark as answered. Thanks! http://stackoverflow.com/questions/35633854/handling-one-time-immutable-data-created-by-app – bpedit Feb 29 '16 at 17:37
  • I don't yet know why but just archiving and retrieving in this manner uses less than half the memory of my previous paradigm and seems, at first glance, not to persist. I've written apps that have accessed several dozen HTML files, don't know why I didn't think to apply it here. Loading is a tad slower but an acceptable tradeoff. Thanks again. – bpedit Feb 29 '16 at 18:15
  • BTW, any suggestions on where to find the generated plist's? I've searched all sub folders in the DerivedData for the current build but find nothing. – bpedit Feb 29 '16 at 18:41
  • Files will be in your user ~/Documents folder within the Simulator's or device's filesystem, or on your Mac if running on OS X. The `print("FilePath"...)` call logs that to your console. – Hal Mueller Feb 29 '16 at 19:00
2

I did also experience a lot of memory bloat from SceneKit in my app, with similar memory chunks as you in Instruments (C3DGenericSourceCreateDeserializedDataWithAccessors, C3DMeshSourceCreateMutable, etc). I found that setting the geometry property to nil on the SCNNode objects before letting Swift deinitialize them solved it.

In your case, in you cleanup function, do something like:

atomsNode_1.removeFromParentNode()
atomsNode_1.geometry = nil
atomsNode_2.removeFromParentNode()
atomsNode_2.geometry = nil

Another example of how you may implement the cleaning:

class ViewController: UIViewController {
    @IBOutlet weak var sceneView: SCNView!
    var scene: SCNScene!

    // ...

    override func viewDidLoad() {
        super.viewDidLoad()
        scene = SCNScene()
        sceneView.scene = scene

        // ...
    }

    deinit {
        scene.rootNode.cleanup()
    }

    // ...
}

extension SCNNode {
    func cleanup() {
        for child in childNodes {
            child.cleanup()
        }
        geometry = nil
    }
}

If that doesn't work, you may have better success by setting its texture to nil, as reported on scene kit memory management using swift.

Daniel Jonsson
  • 3,261
  • 5
  • 45
  • 66
  • Thanks! It's been awhile, I'm not sure of all the improvements that helped. But the biggest factor by far was archiving my model node trees as resources then calling as needed. Thanks to Hal Mueller's help in another thread. – bpedit Jun 27 '17 at 14:25