Using Janitor to combat memory leaks

Janitor is similar to Maid, you can use any of them, Janitor is a more maintained, better library with extra features, and better type checking, and overall to me it gives me a better developer experience.


Before anything, you can get Janitor by clicking here.

Janitor is a library that helps you keep track of things that need to be cleaned.

For example, a common use case, is storing RBXScriptConnections to then disconnect them at a later time, when they’re not needed anymore to prevent functions, instances from never being collected and extra threads possibly being created when not needed.

Why use Janitor?

This topic is not about trying to get you into Janitor, or even have any reference manager. However, if you’re looking for a reason to use Janitor instead of Maid, you can click here to see some of the reasons listed by @pobammer.

I would rather this topic does not turn into a discussion about it. If you wanna have conversation about it, go on #scripting-support and create a topic or find one.


Janitor is a class, so you can create multiple Janitor objects.

Commonly, you’ll have a Janitor for a player, sometimes for an instance, one for a specific script, tool, etc…

Creating a Janitor

You can create a Janitor object by calling Janitor.new.

local janitor = Janitor.new()

We now have a janitor object, what can we do with it?


Adding objects to Janitor

Adding a RBXScriptConnection

You might wanna disconnect a ScriptConnection, you can do that by just calling :Add on the Janitor object passing through the RBXScriptConnection.

janitor:Add(
    BindableEvent.Event:Connect(function()
        print("Fired")
    end)
)

It’s important to note that ScriptConnections created from signals created by libraries like GoodSignal, FastSignal, and others fit into custom objects and not RBXScriptConnections. We will look into how we can do that soon.

Adding an Instance

Janitor has objects that it just automatically understands how to handle, those are RBXScriptConnections and functions. (functions explained later)

However, Instances are not part of that, that just means that you have to tell Janitor how to destroy it. This is simple, and it just consists of passing what the destroy method is called as a string as the second parameter.

janitor:Add(part, "Destroy")

Note: Janitor does auto handle calling :Destroy when nothing is specified, however personally, I believe it is a best practice to always pass the method that needs to be called, as it gets into your muscle memory.

Adding a custom object

Like before, you just have to specify what method needs to be called, usually that will be :Destroy or something else, in this case, in GoodSignal’s native implementation, it’s :DisconnectAll.

janitor:Add(
    GoodSignal.new(), "DisconnectAll"
)

Adding a function

Janitor also supports adding functions to the clean up list, what happens to these functions is that they’re just called when the Janitor is cleaned up.

janitor:Add(function()
    print("This function is called when the object is cleaned up")
end)

Cleaning up a Janitor

Cleaning up a Janitor object means calling all the destroy methods on all the added objects and cleaning the Janitor’s references to these objects.

That are some ways of cleaning up a Janitor object, manually, automatically…

:Cleanup

Calling :Cleanup does just that, no questions asked.

:Destroy

Calling :Destroy on the Janitor object also calls :Cleanup, and makes the janitor object unusable. Call it instead if you’re not gonna be using the object anymore.

Linking an Instance to Janitor

You might have to go through process of destroying cleaning up a Janitor object when an Instance is destroyed.

Thankfully, Janitor has a method that does that for you!
It’s called :LinkToInstance.

janitor:Add(function()
    print("This is called when the Janitor is destroyed")
end)

local part = Instance.new("Part", Workspace)
janitor:LinkToInstance(part)

part:Destroy()
-- Janitor should be destroyed now

Prematurely removing an object

Sometimes you’ll need destroy an object which was inside an object before the Janitor is cleaned up. In that case, you might want or even need to remove it from Janitor.

Removing an object from Janitor means also calling the specified destroy method, there’s no way around this as of writing this.

However, for you to be able to do that, you need to have specified an index for that object when calling :Add. This index can be anything, and it’s the 3rd parameter of :Add.

You can then, call :Remove on the Janitor object with the index of the object.

janitor:Add(
    Signal, "Destroy", "SomeEventThatHandlesSomething"
)

-- Some time later...

janitor:Remove("SomeEventThatHandlesSomething")

Using Janitor in Classes

A common use case for Janitor is using it to clean up anything that an object needs to stay alive, and then clean it up when it’s not needed anymore.

You’ll usually have a _janitor member inside your class to do that.

function Timer.new(goal: number)
    local self = setmetatable({
        TimePassed = 0,
        GoalReached = Signal.new(),

        _goal = goal,
        _janitor = Janitor.new()
    }, Timer)

    self._janitor:Add(self.GoalReached, "Destroy")
    self._janitor:Add(
        RunService.Heartbeat:Connect(function(deltaTime)
            self.TimePassed += deltaTime

            if self.TimePassed >= self._goal then
                self.GoalReached:Fire()
                self:Destroy()
            end
        end), "Disconnect"
    )

    return self
end

function Timer:Destroy()
    local janitor = self._janitor
    if janitor then
        janitor:Destroy()
        self._janitor = nil
    end
end

This is where Janitor really shines, you should give it a try.


:Add returns the object passed

This is a nice add that I couldn’t fit anywhere before on this topic, however it’s nice to know that unlike Maid, Janitor returns the object passed through :Add which is helpful if you’re constructing the object right on the :Add call.

local object = janitor:Add(
     Object.new(), "Destroy", "Index"
)

object:method()

With all that, that’s it! That’s my starter guide to using Janitor, there’s still a lot more info you can get, but this should get you around with using Janitor on your project!

If you wanna look into API methods returns, other parameters, etc, you can look at Janitor’s documentation here.

Most notably, if you use Promise, Janitor has features to interact with it, and it’s worth to read the documentation for those.

49 Likes

Maid isn’t the name of a library, it is literally the name of this type of object (that allows you to queue up objects to be disposed of in the future). Janitor would therefore be a library that offers a type of maid.

1 Like

Yes, you’re correct, however in the Roblox community usually when you’re referencing Janitor & Maid, you’re be referencing Janitor as hms’ library and Maid as Nevermore’s, which is what I mean in this topic.

3 Likes