BubbleChat PlayerScript 'TextSizeCache' memory leak

I’m not sure if the script is technically live yet, but I found it earlier today and noticed that this code doesn’t work as intended:

local testLabel = Instance.new("TextLabel")
testLabel.Selectable = false
testLabel.TextWrapped = true
testLabel.Position = UDim2.new(1, 0, 1, 0)
testLabel.Parent = BubbleChatScreenGui

local TextSizeCache = {}

--[[ FUNCTIONS ]]

local function getStringTextBounds(text, font, textSize, sizeBounds)
	sizeBounds = sizeBounds or false
	if not TextSizeCache[text] then
		TextSizeCache[text] = {}
	end
	if not TextSizeCache[text][font] then
		TextSizeCache[text][font] = {}
	end
	if not TextSizeCache[text][font][sizeBounds] then
		TextSizeCache[text][font][sizeBounds] = {}
	end
	if not TextSizeCache[text][font][sizeBounds][textSize] then
		testLabel.Text = text
		testLabel.Font = font
		testLabel.TextSize = textSize
		if sizeBounds then
			testLabel.TextWrapped = true;
			testLabel.Size = UDim2.new(0, sizeBounds.x, 0, sizeBounds.y)
		else
			testLabel.TextWrapped = false;
		end
		TextSizeCache[text][font][sizeBounds][textSize] = testLabel.TextBounds
	end
	return TextSizeCache[text][font][sizeBounds][textSize]
end

‘sizeBounds’ is a unique Vector2, so the cache just fills up with new vector2’s every time someone chats.

local t = {}
local key1 = Vector2.new(1, 2)
local key2 = Vector2.new(1, 2)
t[key1] = true
print(t[key2])
print(t[key1])

> nil
> true

An okay fix would be to either use sizeBounds.X instead, or have separate tables for x and y.

Here’s a solution using metatables:

local function newCache(getter)
	return setmetatable({}, {
		__index = function(self, k)
			local v = getter(k)
			self[k] = v
			return v
		end
	})
end

local function newClearingCache(duration, getter)
	local mt = {}
	function mt:__index(k)
		setmetatable(self, {__index = newCache(getter)})
		
		spawn(function()
			wait(duration)
			setmetatable(self, mt) -- cleanup
		end)
		
		return self[k]
	end
	return setmetatable({}, mt)
end

local TextBoundsCache = newCache(function(font)
	local label = Instance.new("TextLabel")
	label.Selectable = false
	label.TextWrapped = true
	label.Font = font
	
	return newClearingCache(20, function(height)
		return newCache(function(width)
			local size = UDim2.new(0, width, 0, 512)
			return newCache(function(text)
				label.TextSize = height
				label.Size = size
				label.Text = text
				
				label.Parent = screenGui
				local bounds = label.TextBounds
				label.Parent = nil
				
				label.Text = ""
				
				return bounds
			end)
		end)
	end)
end)

print(TextBoundsCache[Enum.Font.SourceSans][18][64]["Hello world!"])

@TheGamer101

2 Likes

Well found. We use this same getStringTextBounds code in a bunch of places, so this memory leak actually currently happens. I think we will probably just remove caching altogether, because it wasn’t working for so long and we never noticed. We should just use TextService:GetTextSize everywhere, since this has been unlocked.

4 Likes