update todo
[QuestHelper.git] / recycle.lua
blob00bd28f4c36b4ed8845ca0271b572c8e65e0cd29
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 wipe(tbl)
103 self.used_tables = self.used_tables - 1
104 self.recycle_tabletyping[tbl] = nil
106 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
107 self.free_tables[setmetatable(tbl, unused_meta)] = true
108 release_cycle = release_cycle + 1
109 else
110 self.recycle_tabletyping[tbl] = (self.recycle_tabletyping[tbl] or "((unknown))") .. "((released))"
111 release_cycle = 0
115 function QuestHelper:RecycleClear()
116 local ct = QuestHelper:TableSize(QuestHelper.free_tables)
117 QuestHelper.free_tables = {}
118 return ct
121 function QuestHelper:DumpTableTypeFrequencies(silent)
122 local freq = {}
123 for k, v in pairs(self.recycle_tabletyping) do
124 freq[v] = (freq[v] or 0) + 1
127 if not silent then
128 local flist = {}
129 for k, v in pairs(freq) do
130 table.insert(flist, {count=v, name=k})
133 table.sort(flist, function(a, b) return a.count < b.count end)
135 for k, v in pairs(flist) do
136 self:TextOut(v.count .. ": " .. v.name)
140 return freq
143 function QuestHelper:CreateFrame(parent)
144 self.used_frames = self.used_frames + 1
145 local frame = table.remove(self.free_frames)
147 if frame then
148 frame:SetParent(parent)
149 else
150 frame = CreateFrame("Button", string.format("QuestHelperFrame%d",self.used_frames), parent)
153 frame:SetFrameLevel((parent or UIParent):GetFrameLevel()+1)
154 frame:SetFrameStrata("MEDIUM")
155 frame:Show()
157 return frame
160 local frameScripts =
162 "OnChar",
163 "OnClick",
164 "OnDoubleClick",
165 "OnDragStart",
166 "OnDragStop",
167 "OnEnter",
168 "OnEvent",
169 "OnHide",
170 "OnKeyDown",
171 "OnKeyUp",
172 "OnLeave",
173 "OnLoad",
174 "OnMouseDown",
175 "OnMouseUp",
176 "OnMouseWheel",
177 "OnReceiveDrag",
178 "OnShow",
179 "OnSizeChanged",
180 "OnUpdate",
181 "PostClick",
182 "PreClick"
185 function QuestHelper:ReleaseFrame(frame)
186 assert(type(frame) == "table")
187 for i,t in ipairs(self.free_frames) do assert(t ~= frame) end
189 for key in pairs(frame) do
190 -- Remove all keys except 0, which seems to hold some special data.
191 if key ~= 0 then
192 frame[key] = nil
196 for _, script in ipairs(frameScripts) do
197 QH_Hook(frame, script, nil)
200 frame:Hide()
201 frame:SetParent(QuestHelper)
202 frame:ClearAllPoints()
203 frame:SetMovable(false)
204 frame:RegisterForDrag()
205 frame:RegisterForClicks()
206 frame:SetBackdrop(nil)
207 frame:SetScale(1)
208 frame:SetAlpha(1)
210 self.used_frames = self.used_frames - 1
211 table.insert(self.free_frames, frame)
214 function QuestHelper:CreateText(parent, text_str, text_size, text_font, r, g, b, a)
215 self.used_text = self.used_text + 1
216 local text = table.remove(self.free_text)
218 if text then
219 text:SetParent(parent)
220 else
221 text = parent:CreateFontString()
224 text:SetFont(text_font or QuestHelper.font.sans or ChatFontNormal:GetFont(), text_size or 12)
225 text:SetDrawLayer("OVERLAY")
226 text:SetJustifyH("CENTER")
227 text:SetJustifyV("MIDDLE")
228 text:SetTextColor(r or 1, g or 1, b or 1, a or 1)
229 text:SetText(text_str or "")
230 text:SetShadowColor(0, 0, 0, 0.3)
231 text:SetShadowOffset(1, -1)
232 text:Show()
234 return text
237 function QuestHelper:ReleaseText(text)
238 assert(type(text) == "table")
239 for i,t in ipairs(self.free_text) do assert(t ~= text) end
241 for key in pairs(text) do
242 -- Remove all keys except 0, which seems to hold some special data.
243 if key ~= 0 then
244 text[key] = nil
248 text:Hide()
249 text:SetParent(UIParent)
250 text:ClearAllPoints()
251 self.used_text = self.used_text - 1
252 table.insert(self.free_text, text)
255 function QuestHelper:CreateTexture(parent, r, g, b, a)
256 self.used_textures = self.used_textures + 1
257 local tex = table.remove(self.free_textures)
259 if tex then
260 tex:SetParent(parent)
261 else
262 tex = parent:CreateTexture()
265 if not tex:SetTexture(r, g, b, a) and
266 not tex:SetTexture("Interface\\Icons\\Temp.blp") then
267 tex:SetTexture(1, 0, 1, 0.5)
270 tex:ClearAllPoints()
271 tex:SetTexCoord(0, 1, 0, 1)
272 tex:SetVertexColor(1, 1, 1, 1)
273 tex:SetDrawLayer("ARTWORK")
274 tex:SetBlendMode("BLEND")
275 tex:SetWidth(12)
276 tex:SetHeight(12)
277 tex:Show()
279 return tex
282 function QuestHelper:CreateIconTexture(parent, id)
283 local icon = self:CreateTexture(parent, "Interface\\AddOns\\QuestHelper\\Art\\Icons.tga")
285 local w, h = 1/8, 1/8
286 local x, y = ((id-1)%8)*w, math.floor((id-1)/8)*h
288 icon:SetTexCoord(x, x+w, y, y+h)
290 return icon
293 function QuestHelper:CreateDotTexture(parent)
294 local icon = self:CreateIconTexture(parent, 13)
295 icon:SetWidth(5)
296 icon:SetHeight(5)
297 icon:SetVertexColor(0, 0, 0, 0.35)
298 return icon
301 function QuestHelper:CreateGlowTexture(parent)
302 local tex = self:CreateTexture(parent, "Interface\\Addons\\QuestHelper\\Art\\Glow.tga")
304 local angle = math.random()*6.28318530717958647692528676655900576839433879875021164
305 local x, y = math.cos(angle)*0.707106781186547524400844362104849039284835937688474036588339869,
306 math.sin(angle)*0.707106781186547524400844362104849039284835937688474036588339869
308 -- Randomly rotate the texture, so they don't all look the same.
309 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)
310 tex:ClearAllPoints()
312 return tex
315 function QuestHelper:ReleaseTexture(tex)
316 assert(type(tex) == "table")
317 for i,t in ipairs(self.free_textures) do assert(t ~= tex) end
319 for key in pairs(tex) do
320 -- Remove all keys except 0, which seems to hold some special data.
321 if key ~= 0 then
322 tex[key] = nil
326 tex:Hide()
327 tex:SetParent(UIParent)
328 tex:ClearAllPoints()
329 self.used_textures = self.used_textures - 1
330 table.insert(self.free_textures, tex)
333 QuestHelper.recycle_active_cached_tables = {}
334 QuestHelper.recycle_decache_queue = {}
336 function QuestHelper:CacheRegister(obj)
337 if not self.recycle_active_cached_tables[obj] then
338 self.recycle_active_cached_tables[obj] = true
339 table.insert(self.recycle_decache_queue, obj)
343 function QuestHelper:CacheCleanup(obj)
344 local target = self.recycle_decache_queue[1]
346 if not target then return end
347 table.remove(self.recycle_decache_queue, 1)
348 self.recycle_active_cached_tables[target] = nil
350 if target.distance_cache then
351 for k, v in pairs(target.distance_cache) do
352 self:ReleaseTable(v)
354 self:ReleaseTable(target.distance_cache)
355 target.distance_cache = self:CreateTable("objective.distance_cache cleaned")
359 function QuestHelper:DumpCacheData(obj)
360 local caches = 0
361 local cached = 0
362 for k, v in pairs(self.recycle_decache_queue) do
363 caches = caches + 1
364 if v.distance_cache then
365 for q, w in pairs(v.distance_cache) do
366 cached = cached + 1
371 self:TextOut(caches .. " queued caches with a total of " .. cached .. " cached items")