Memory leaks in the Swift Playground

Apple’s latest language Swift supports the same ObjectiveC ARC / object deletion model where an object is scheduled for deletion once its reference count drops to zero. In their Playground however, objects seems to be held on to far beyond their reference-counted lifetime should permit. It is also unclear what causes objects to be actually deleted subsequent to their zeroth reference. We compared ARC/deletion of objection in an app scenario and within the Playground. The app scenario deletes objects right on cue (zeroth reference) whereas the Playground scenario deletes a few but holds on to a majority of objects (irrespective of scope, but apparently consistent).

The Test Code

We used the following Swift test code (see gist for full listing). First, a few globals for instrumentation & pretty printing:
import Foundation

var activeObjects = Int[]()  //tracks active object ids
var id = 1000                //object id

var indentLevel = 0
func indent(){
  indentLevel =2
}

func outdent(){
  indentLevel -= 2
}
The Info class provides name, id and consistent logging for our objects as well as active object tracking through the Swift init/deinit pair
class Info {
  var name: String
  var uuid =   id

  init(name: String){
    self.name = name
    log("\(self.name)/init()")
    activeObjects  = uuid
  }

  deinit{
    log("\(self.name)/deinit()")
    if let idx = find(activeObjects, self.uuid) {
      activeObjects.removeAtIndex(idx)
    }
    else {
      log("odd: I wasn't in the active objects list!")
    }

  }

  func log(msg: String){
    var ind = ""
    for i in 0..indentLevel {
      ind  = "."
    }
    println("\(uuid) -- \(ind)\(msg)")
  }
}
During the test, we create instances of class X. Not much to see here.
class X : Info {
  init(prefix : String){
    super.init(name: "\(prefix):X")
  }
}
We test two basic allocation scenarios: allocated within a method scope, and provided from an external scope:
class Test : Info{
  init(prefix: String){
    super.init(name: "\(prefix):Test")
  }

Test 1: Local Scope

Create an instance of X within the function as local scope. Per ARC, we expect that the instance is deleted when the call returns.
  func test1() {
    log("\(name)/test1()")
    indent()
    var x : X?
    x=X(prefix: "\(name)/test1")
    x=nil
    test2(X(prefix: name))
    outdent()
  }

Test 2: External Scope

Object is supplied as a parameter (created in an outer context). Per ARC, we expect the object to remain alive when the call returns.
  func test2(x: X){
    log("\(name)/test2()")
  }

Test 3: Nested Local Scope

Test 3: Nested calls to tests 1 and 2 where we create another Test instance in local scope. Per ARC, we expect similar behavior from tests 1 and 2 and a further deletion of the local scope Test object.
  func test3(){
    log("\(name)/test3()")
    indent()
    var t = Test(prefix: "\(name)/test3")
    t.test1()
    outdent() 
    } 

Test 4: Nested External Scope

A nested test, similar to Test 3, but we receive the nested test from the outer scope instead. Per ARC, we expect similar behavior from tests 1 and 2, but the supplied test object will remain following return.
  func test4(t: Test){
    log("\(name)/test4()")
    indent()
    t.test1()
    t.test2(X(prefix: "\(name)/test4"))
    t.test3()
    outdent()
  }
}

Test Runner

The last part is a test runner
class Runner {
  init(){
    var prefix = ""
    var t : Test?
    t = Test(prefix: prefix)
    t!.test1()
    println("")
    t!.test2(X(prefix: prefix))
    println("")
    t!.test3()
    println("")
    t!.test4(Test(prefix: prefix))

Force deallocate the main test object (since it is an optional, we assign nil to it):
    t = nil
Finally, dump the list of active Ids (objects whose deinit{} block hasn’t been called yet):
    // Dump the list of active objects. There should be none at this point!
    println("\nactive objects: ")
    for item in activeObjects {
      println (item)
    }
  }
}
And invoke the runner
var runner = Runner()

Results from App

In an app context (as a local variable of the app delegate), we see the following output which is consistent with ARC and scoping rules:
1001 -- :Test/init()
1001 -- :Test/test1()
1002 -- ..:Test/test1:X/init()
1002 -- ..:Test/test1:X/deinit()
1003 -- ..:Test:X/init()
1001 -- ..:Test/test2()
1003 -- ..:Test:X/deinit()

1004 -- :X/init()
1001 -- :Test/test2()
1004 -- :X/deinit()

1001 -- :Test/test3()
1005 -- ..:Test/test3:Test/init()
1005 -- ..:Test/test3:Test/test1()
1006 -- ....:Test/test3:Test/test1:X/init()
1006 -- ....:Test/test3:Test/test1:X/deinit()
1007 -- ....:Test/test3:Test:X/init()
1005 -- ....:Test/test3:Test/test2()
1007 -- ....:Test/test3:Test:X/deinit()
1005 -- :Test/test3:Test/deinit()

1008 -- :Test/init()
1001 -- :Test/test4()
1008 -- ..:Test/test1()
1009 -- ....:Test/test1:X/init()
1009 -- ....:Test/test1:X/deinit()
1010 -- ....:Test:X/init()
1008 -- ....:Test/test2()
1010 -- ....:Test:X/deinit()
1011 -- ..:Test/test4:X/init()
1008 -- ..:Test/test2()
1011 -- ..:Test/test4:X/deinit()
1008 -- ..:Test/test3()
1012 -- ....:Test/test3:Test/init()
1012 -- ....:Test/test3:Test/test1()
1013 -- ......:Test/test3:Test/test1:X/init()
1013 -- ......:Test/test3:Test/test1:X/deinit()
1014 -- ......:Test/test3:Test:X/init()
1012 -- ......:Test/test3:Test/test2()
1014 -- ......:Test/test3:Test:X/deinit()
1012 -- ..:Test/test3:Test/deinit()
1008 -- :Test/deinit()
1001 -- :Test/deinit()

active objects:
 --empty-- 

Results from Playground

In contrast, when executed in the Playground we see many objects live past their logical ARC deletion point:
1001 -- :Test/init()
1001 -- :Test/test1()
1002 -- ..:Test/test1:X/init()
1003 -- ..:Test:X/init()
1001 -- ..:Test/test2()
1003 -- ..:Test:X/deinit()

1004 -- :X/init()
1001 -- :Test/test2()
1004 -- :X/deinit()

1001 -- :Test/test3()
1005 -- ..:Test/test3:Test/init()
1005 -- ..:Test/test3:Test/test1()
1006 -- ....:Test/test3:Test/test1:X/init()
1007 -- ....:Test/test3:Test:X/init()
1005 -- ....:Test/test3:Test/test2()
1007 -- ....:Test/test3:Test:X/deinit()

1008 -- :Test/init()
1001 -- :Test/test4()
1008 -- ..:Test/test1()
1009 -- ....:Test/test1:X/init()
1010 -- ....:Test:X/init()
1008 -- ....:Test/test2()
1010 -- ....:Test:X/deinit()
1011 -- ..:Test/test4:X/init()
1008 -- ..:Test/test2()
1011 -- ..:Test/test4:X/deinit()
1008 -- ..:Test/test3()
1012 -- ....:Test/test3:Test/init()
1012 -- ....:Test/test3:Test/test1()
1013 -- ......:Test/test3:Test/test1:X/init()
1014 -- ......:Test/test3:Test:X/init()
1012 -- ......:Test/test3:Test/test2()
1014 -- ......:Test/test3:Test:X/deinit()

active objects:
1001
1002
1005
1006
1008
1009
1012
1013

 Probable Causes

One possibility for why the Playground holds on to multiple objects in a seemingly ad hoc fashion is so that it can service its timeline GUI (see image below). Timeline

Here be Dragons

Swift’s playground is a great tool for rapid prototyping, but side effects such as holding on to deallocated objects (if indeed this isn’t a bug), can be rather disconcerting on one hand to outright frustrating when expected behavior breaks.  We’d love to see a warning, or preferably a means to opt-out of side effects in upcoming releases.
  • Thanks, I was playing with swift and the first thing I try to see ARC in action was the playground. I wil try another way