Skip to content
FutureBlitz

    Install

    Null safety and memory, 5 of 5

    Leak reports

    Read the report a program prints when it ends with objects still alive, find the cycle, and fix it.

    What you’ll build

    A small task tracker in which each task knows its project and each project its tasks. Its first version leaks. You’ll read the report that names the cycle, fix it with a weak reference, and see the report go away.

    A program that leaks

    When a program ends with objects still alive, it prints how many of each type there are, on standard error. A debug build tracks every object, so it also shows each cycle, field by field, from an object back to itself.

    tasks.fb
    Import std.core
    
    Class Project
        ReadOnly Field Name: String
        Field Tasks: Task[] = []
    
        New(name: String)
            Name = name
        End New
    
        Method Add(title: String)
            Tasks.Add(Task(title, Self))
        End Method
    End Class
    
    Class Task
        ReadOnly Field Title: String
        Field Owner: Project
    
        New(title: String, owner: Project)
            Title = title
            Owner = owner
        End New
    End Class
    
    Local site = Project("website")
    site.Add("write the copy")
    site.Add("check the links")
    Print("{site.Name} has {site.Tasks.Count} tasks")
    Output
    website has 2 tasks
    leak: 1 Project object was never released
    leak: 2 Task objects were never released
    leak: 1 Task[] object was never released
    leak: a cycle of 3 objects: Project.Tasks -> Task[] element -> Task.Owner -> the first Project

    Each line of the report is a fact about what was left:

    • The first three lines count what was still alive when the program ended, by type: the Project, its two Tasks, and the Task[] array that holds them.
    • leak: a cycle of 3 objects follows the references around one loop. The Project’s Tasks holds the array, the array holds a Task, and the Task’s Owner holds the first Project again.

    The cycle says which field to change: one of the links in it has to stop owning what it points at. A task belongs to its project, not the other way round, so Owner is the link to weaken. A release build prints the counts alone, without the cycles.

    Fixing the cycle

    With Owner a Weak<Project>, the tasks no longer keep their project alive. Reading it gives a Project?, since the project could be gone by then.

    tasks.fb
    Import std.core
    
    Class Project
        ReadOnly Field Name: String
        Field Tasks: Task[] = []
    
        New(name: String)
            Name = name
        End New
    
        Method Add(title: String)
            Tasks.Add(Task(title, Self))
        End Method
    End Class
    
    Class Task
        ReadOnly Field Title: String
        Field Owner: Weak<Project>
    
        New(title: String, owner: Project)
            Title = title
            Owner = owner
        End New
    End Class
    
    Local site = Project("website")
    site.Add("write the copy")
    site.Add("check the links")
    Print("{site.Name} has {site.Tasks.Count} tasks")
    Print("the first belongs to {site.Tasks[0].Owner?.Name ?? "no project"}")
    Output
    website has 2 tasks
    the first belongs to website

    The program ends without a report: every object was freed.

    Leaks in tests

    fb test checks for leaks too. In a debug build, a test that leaves objects alive fails, and lists what it left, so the leak is found by the test that made it. A test’s list counts what was left, without the cycles a program’s report follows. Here a test catches the first version’s cycle, and fb test exits with code 1:

    tasks.fb
    Import std.core
    Import std.test
    
    Class Project
        Field Tasks: Task[] = []
    
        Method Add()
            Tasks.Add(Task(Self))
        End Method
    End Class
    
    Class Task
        Field Owner: Project
    
        New(owner: Project)
            Owner = owner
        End New
    End Class
    
    @Test
    Function ProjectsFreeTheirTasks()
        Local project = Project()
        project.Add()
        Expect(project.Tasks.Count == 1)
    End Function
    Output
         Running 1 test
    test tasks.ProjectsFreeTheirTasks ... FAILED
    
    failures:
    
    ---- tasks.ProjectsFreeTheirTasks ----
    leak: 1 Project object was never released
    leak: 1 Task[] object was never released
    leak: 1 Task object was never released
    
    test result: FAILED. 0 passed; 1 failed; 0 skipped; 0 filtered out; finished in 0.02s
    leak: 1 Project object was never released
    leak: 1 Task object was never released
    leak: 1 Task[] object was never released
    leak: a cycle of 3 objects: Project.Tasks -> Task[] element -> Task.Owner -> the first Project

    What you learned

    • A program that ends with objects alive prints a leak report, with counts by type.
    • A debug build’s report also follows each cycle field by field.
    • The cycle shows which reference to make weak: the one that shouldn’t own its object.
    • fb test fails a test that leaks.

    Report an issue with this page or with FutureBlitz on GitHub.