avoid a crash
[QuestHelper.git] / recycle.lua
blob94ab0041f09c61caa70c23b8db56110c24808c5d
1 QuestHelper_File["recycle.lua"] = "Development Version"
2 QuestHelper_Loadtime["recycle.lua"] = GetTime()
4 --[[
6 "Zorba, why are you doing manual memory allocation in Lua? That's incredibly stupid! You, as well, must be incredibly stupid. Why are you so stupid?"
8 Yeah. Yeah, that's what I thought too. It turns out things are more complicated than I thought.
10 There's a few good reasons to do something this ugly.
12 First off, it makes it real, real easy to track where allocations are going. That's what the whole "tag" thing is for - all created tables are tagged. This is useful. This is very, very useful, as it lets me track down memory leaks extraordinarily easily. This is also obsoleted slightly by the technique in bst_pre.lua (check it out.)
14 Second, it deals far better with table churn. I don't know if this is a WoW issue, but in WoW at least, tables can hang around for quite a while before getting garbage-collected. If you're making a dozen tables per frame, you can rapidly eat 10 or 20 megs of RAM that you're not actually using. Rigging an explicit thing like this allows you to recycle those tables instead of just wasting them.
16 It's ugly. I'm not arguing that. But it really, really helps.
20 QuestHelper.used_tables = 0
21 QuestHelper.free_tables = setmetatable({}, {__mode="k"}) -- But Zorba, the only thing you're storing here is unused table values! Yeah, that's right, *unused* table values, if the garbage collector wants to have a field day, go for it
23 local function crashy(tab, name)
24 QuestHelper: Assert(false, "Tried to access " .. name .. " from released table")
25 end
27 local unused_meta = {__index=crashy, __newindex=crashy}
29 QuestHelper.used_textures = 0
30 QuestHelper.free_textures = {}
32 QuestHelper.used_text = 0
33 QuestHelper.free_text = {}
35 QuestHelper.used_frames = 0
36 QuestHelper.free_frames = {}
38 -- This little table rigs up a basic typing system to assist with debugging. It has weak-reference keys so it shouldn't ever lead to leaks of any kind.
39 QuestHelper.recycle_tabletyping = setmetatable({}, {__mode="k"})
41 local toomanytables_warned = false
42 local function mark(table, item, tag) table[item] = tag end
43 function QuestHelper:CreateTable(tag)
44 local tbl = next(self.free_tables)
45 self.used_tables = self.used_tables + 1
47 if not tbl then
48 tbl = {}
49 else
50 self.free_tables[tbl] = nil
51 setmetatable(tbl, nil)
52 end
54 tag = tag or string.gsub(debugstack(2, 1, 1), "\n.*", "")
55 if type(tag) ~= "string" then tag = tostring(tag) .. " ((weird)) " .. string.gsub(debugstack(2, 1, 1), "\n.*", "") end
57 if QH_RegisterTable then QH_RegisterTable(tbl, true, tag) end
58 if not pcall(mark, self.recycle_tabletyping, tbl, tag) then
59 local freq = {}
60 for _, v in pairs(self.recycle_tabletyping) do
61 freq[v] = (freq[v] or 0) + 1
62 end
64 local fqx = {}
65 for k, v in pairs(freq) do
66 table.insert(fqx, {k, v})
67 end
69 table.sort(fqx, function(a, b) return a[2] < b[2] end)
71 local stt = "recycle overflow error (too many tables)\n"
73 for _, v in ipairs(fqx) do
74 stt = stt .. string.format(" %d: %s\n", v[2], v[1])
75 end
77 local pcscaught = QH_ClearPathcache(true)
78 collectgarbage("collect")
80 stt = stt .. string.format(" (pathcache cleared %d)\n", pcscaught)
82 if not pcall(mark, self.recycle_tabletyping, tbl, tag) then
83 QuestHelper: Assert(false, stt)
84 end
86 QuestHelper_ErrorCatcher_ExplicitError(false, stt .. " (recovered)\n")
87 if not toomanytables_warned then
88 QuestHelper:TextOut("Something has gone wrong! QuestHelper should continue working, but Zorba would really appreciate it if you type |cffbbffd6/qh error|r and went to report that on the QuestHelper homepage.")
89 toomanytables_warned = true
90 end
91 end
93 return tbl
94 end
96 local release_cycle = 0
97 function QuestHelper:ReleaseTable(tbl)
98 QuestHelper: Assert(type(tbl) == "table")
99 QuestHelper: Assert(not self.free_tables[tbl])
101 for key in pairs(tbl) do
102 tbl[key] = nil
105 self.used_tables = self.used_tables - 1
106 self.recycle_tabletyping[tbl] = nil
108 if QH_RegisterTable or self.used_tables < 500 or release_cycle < 100 then -- this is actually plenty. you'd be horrified how much table churn there is in this thing
109 self.free_tables[setmetatable(tbl, unused_meta)] = true
110 release_cycle = release_cycle + 1
111 else
112 self.recycle_tabletyping[tbl] = (self.recycle_tabletyping[tbl] or "((unknown))") .. "((released))"
113 release_cycle = 0
117 function QuestHelper:RecycleClear()
118 local ct = QuestHelper:TableSize(QuestHelper.free_tables)
119 QuestHelper.free_tables = {}
120 return ct
123 function QuestHelper:DumpTableTypeFrequencies(silent)
124 local freq = {}
125 for k, v in pairs(self.recycle_tabletyping) do
126 freq[v] = (freq[v] or 0) + 1
129 if not silent then
130 local flist = {}
131 for k, v in pairs(freq) do
132 table.insert(flist, {count=v, name=k})
135 table.sort(flist, function(a, b) return a.count < b.count end)
137 for k, v in pairs(flist) do
138 self:TextOut(v.count .. ": " .. v.name)
142 return freq
145 function QuestHelper:CreateFrame(parent)
146 self.used_frames = self.used_frames + 1
147 local frame = table.remove(self.free_frames)
149 if frame then
150 frame:SetParent(parent)
151 else
152 frame = CreateFrame("Button", string.format("QuestHelperFrame%d",self.used_frames), parent)
155 frame:SetFrameLevel((parent or UIParent):GetFrameLevel()+1)
156 frame:SetFrameStrata("MEDIUM")
157 frame:Show()
159 return frame
162 local frameScripts =
164 "OnChar",
165 "OnClick",
166 "OnDoubleClick",
167 "OnDragStart",
168 "OnDragStop",
169 "OnEnter",
170 "OnEvent",
171 "OnHide",
172 "OnKeyDown",
173 "OnKeyUp",
174 "OnLeave",
175 "OnLoad",
176 "OnMouseDown",
177 "OnMouseUp",
178 "OnMouseWheel",
179 "OnReceiveDrag",
180 "OnShow",
181 "OnSizeChanged",
182 "OnUpdate",
183 "PostClick",
184 "PreClick"
187 function QuestHelper:ReleaseFrame(frame)
188 assert(type(frame) == "table")
189 for i,t in ipairs(self.free_frames) do assert(t ~= frame) end
191 for key in pairs(frame) do
192 -- Remove all keys except 0, which seems to hold some special data.
193 if key ~= 0 then
194 frame[key] = nil
198 for _, script in ipairs(frameScripts) do
199 QH_Hook(frame, script, nil)
202 frame:Hide()
203 frame:SetParent(QuestHelper)
204 frame:ClearAllPoints()
205 frame:SetMovable(false)
206 frame:RegisterForDrag()
207 frame:RegisterForClicks()
208 frame:SetBackdrop(nil)
209 frame:SetScale(1)
210 frame:SetAlpha(1)
212 self.used_frames = self.used_frames - 1
213 table.insert(self.free_frames, frame)
216 function QuestHelper:CreateText(parent, text_str, text_size, text_font, r, g, b, a)
217 self.used_text = self.used_text + 1
218 local text = table.remove(self.free_text)
220 if text then
221 text:SetParent(parent)
222 else
223 text = parent:CreateFontString()
226 text:SetFont(text_font or QuestHelper.font.sans or ChatFontNormal:GetFont(), text_size or 12)
227 text:SetDrawLayer("OVERLAY")
228 text:SetJustifyH("CENTER")
229 text:SetJustifyV("MIDDLE")
230 text:SetTextColor(r or 1, g or 1, b or 1, a or 1)
231 text:SetText(text_str or "")
232 text:SetShadowColor(0, 0, 0, 0.3)
233 text:SetShadowOffset(1, -1)
234 text:Show()
236 return text
239 function QuestHelper:ReleaseText(text)
240 assert(type(text) == "table")
241 for i,t in ipairs(self.free_text) do assert(t ~= text) end
243 for key in pairs(text) do
244 -- Remove all keys except 0, which seems to hold some special data.
245 if key ~= 0 then
246 text[key] = nil
250 text:Hide()
251 text:SetParent(UIParent)
252 text:ClearAllPoints()
253 self.used_text = self.used_text - 1
254 table.insert(self.free_text, text)
257 function QuestHelper:CreateTexture(parent, r, g, b, a)
258 self.used_textures = self.used_textures + 1
259 local tex = table.remove(self.free_textures)
261 if tex then
262 tex:SetParent(parent)
263 else
264 tex = parent:CreateTexture()
267 if not tex:SetTexture(r, g, b, a) and
268 not tex:SetTexture("Interface\\Icons\\Temp.blp") then
269 tex:SetTexture(1, 0, 1, 0.5)
272 tex:ClearAllPoints()
273 tex:SetTexCoord(0, 1, 0, 1)
274 tex:SetVertexColor(1, 1, 1, 1)
275 tex:SetDrawLayer("ARTWORK")
276 tex:SetBlendMode("BLEND")
277 tex:SetWidth(12)
278 tex:SetHeight(12)
279 tex:Show()
281 return tex
284 function QuestHelper:CreateIconTexture(parent, id)
285 local icon = self:CreateTexture(parent, "Interface\\AddOns\\QuestHelper\\Art\\Icons.tga")
287 local w, h = 1/8, 1/8
288 local x, y = ((id-1)%8)*w, math.floor((id-1)/8)*h
290 icon:SetTexCoord(x, x+w, y, y+h)
292 return icon
295 function QuestHelper:CreateDotTexture(parent)
296 local icon = self:CreateIconTexture(parent, 13)
297 icon:SetWidth(5)
298 icon:SetHeight(5)
299 icon:SetVertexColor(0, 0, 0, 0.35)
300 return icon
303 function QuestHelper:CreateGlowTexture(parent)
304 local tex = self:CreateTexture(parent, "Interface\\Addons\\QuestHelper\\Art\\Glow.tga")
306 local angle = math.random()*6.28318530717958647692528676655900576839433879875021164
307 local x, y = math.cos(angle)*0.707106781186547524400844362104849039284835937688474036588339869,
308 math.sin(angle)*0.707106781186547524400844362104849039284835937688474036588339869
310 -- Randomly rotate the texture, so they don't all look the same.
311 tex:SetTexCoord(x+0.5, y+0.5, y+0.5, 0.5-x, 0.5-y, x+0.5, 0.5-x, 0.5-y)
312 tex:ClearAllPoints()
314 return tex
317 function QuestHelper:ReleaseTexture(tex)
318 assert(type(tex) == "table")
319 for i,t in ipairs(self.free_textures) do assert(t ~= tex) end
321 for key in pairs(tex) do
322 -- Remove all keys except 0, which seems to hold some special data.
323 if key ~= 0 then
324 tex[key] = nil
328 tex:Hide()
329 tex:SetParent(UIParent)
330 tex:ClearAllPoints()
331 self.used_textures = self.used_textures - 1
332 table.insert(self.free_textures, tex)
335 QuestHelper.recycle_active_cached_tables = {}
336 QuestHelper.recycle_decache_queue = {}
338 function QuestHelper:CacheRegister(obj)
339 if not self.recycle_active_cached_tables[obj] then
340 self.recycle_active_cached_tables[obj] = true
341 table.insert(self.recycle_decache_queue, obj)
345 function QuestHelper:CacheCleanup(obj)
346 local target = self.recycle_decache_queue[1]
348 if not target then return end
349 table.remove(self.recycle_decache_queue, 1)
350 self.recycle_active_cached_tables[target] = nil
352 if target.distance_cache then
353 for k, v in pairs(target.distance_cache) do
354 self:ReleaseTable(v)
356 self:ReleaseTable(target.distance_cache)
357 target.distance_cache = self:CreateTable("objective.distance_cache cleaned")
361 function QuestHelper:DumpCacheData(obj)
362 local caches = 0
363 local cached = 0
364 for k, v in pairs(self.recycle_decache_queue) do
365 caches = caches + 1
366 if v.distance_cache then
367 for q, w in pairs(v.distance_cache) do
368 cached = cached + 1
373 self:TextOut(caches .. " queued caches with a total of " .. cached .. " cached items")