more debuggery
[QuestHelper.git] / recycle.lua
bloba2061e52a31988ad8b49c7abc6d8b14f9192567e
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 function QuestHelper:CreateTable(tag)
42 local tbl = next(self.free_tables)
43 self.used_tables = self.used_tables + 1
45 if not tbl then
46 tbl = {}
47 else
48 self.free_tables[tbl] = nil
49 setmetatable(tbl, nil)
50 end
52 tag = tag or string.gsub(debugstack(2, 1, 1), "\n.*", "")
54 if QH_RegisterTable then QH_RegisterTable(tbl, true, tag) end
55 self.recycle_tabletyping[tbl] = tag
57 return tbl
58 end
60 local release_cycle = 0
61 function QuestHelper:ReleaseTable(tbl)
62 QuestHelper: Assert(type(tbl) == "table")
63 QuestHelper: Assert(not self.free_tables[tbl])
65 for key in pairs(tbl) do
66 tbl[key] = nil
67 end
69 self.used_tables = self.used_tables - 1
70 self.recycle_tabletyping[tbl] = nil
72 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
73 self.free_tables[setmetatable(tbl, unused_meta)] = true
74 release_cycle = release_cycle + 1
75 else
76 self.recycle_tabletyping[tbl] = "((released))"
77 release_cycle = 0
78 end
79 end
81 function QuestHelper:RecycleClear()
82 local ct = QuestHelper:TableSize(QuestHelper.free_tables)
83 QuestHelper.free_tables = {}
84 return ct
85 end
87 function QuestHelper:DumpTableTypeFrequencies(silent)
88 local freq = {}
89 for k, v in pairs(self.recycle_tabletyping) do
90 freq[v] = (freq[v] or 0) + 1
91 end
93 if not silent then
94 local flist = {}
95 for k, v in pairs(freq) do
96 table.insert(flist, {count=v, name=k})
97 end
99 table.sort(flist, function(a, b) return a.count < b.count end)
101 for k, v in pairs(flist) do
102 self:TextOut(v.count .. ": " .. v.name)
106 return freq
109 function QuestHelper:CreateFrame(parent)
110 self.used_frames = self.used_frames + 1
111 local frame = table.remove(self.free_frames)
113 if frame then
114 frame:SetParent(parent)
115 else
116 frame = CreateFrame("Button", string.format("QuestHelperFrame%d",self.used_frames), parent)
119 frame:SetFrameLevel((parent or UIParent):GetFrameLevel()+1)
120 frame:SetFrameStrata("MEDIUM")
121 frame:Show()
123 return frame
126 local frameScripts =
128 "OnChar",
129 "OnClick",
130 "OnDoubleClick",
131 "OnDragStart",
132 "OnDragStop",
133 "OnEnter",
134 "OnEvent",
135 "OnHide",
136 "OnKeyDown",
137 "OnKeyUp",
138 "OnLeave",
139 "OnLoad",
140 "OnMouseDown",
141 "OnMouseUp",
142 "OnMouseWheel",
143 "OnReceiveDrag",
144 "OnShow",
145 "OnSizeChanged",
146 "OnUpdate",
147 "PostClick",
148 "PreClick"
151 function QuestHelper:ReleaseFrame(frame)
152 assert(type(frame) == "table")
153 for i,t in ipairs(self.free_frames) do assert(t ~= frame) end
155 for key in pairs(frame) do
156 -- Remove all keys except 0, which seems to hold some special data.
157 if key ~= 0 then
158 frame[key] = nil
162 for i, script in ipairs(frameScripts) do
163 frame:SetScript(script, nil)
166 frame:Hide()
167 frame:SetParent(QuestHelper)
168 frame:ClearAllPoints()
169 frame:SetMovable(false)
170 frame:RegisterForDrag()
171 frame:RegisterForClicks()
172 frame:SetBackdrop(nil)
173 frame:SetScale(1)
174 frame:SetAlpha(1)
176 self.used_frames = self.used_frames - 1
177 table.insert(self.free_frames, frame)
180 function QuestHelper:CreateText(parent, text_str, text_size, text_font, r, g, b, a)
181 self.used_text = self.used_text + 1
182 local text = table.remove(self.free_text)
184 if text then
185 text:SetParent(parent)
186 else
187 text = parent:CreateFontString()
190 text:SetFont(text_font or QuestHelper.font.sans or ChatFontNormal:GetFont(), text_size or 12)
191 text:SetDrawLayer("OVERLAY")
192 text:SetJustifyH("CENTER")
193 text:SetJustifyV("MIDDLE")
194 text:SetTextColor(r or 1, g or 1, b or 1, a or 1)
195 text:SetText(text_str or "")
196 text:SetShadowColor(0, 0, 0, 0.3)
197 text:SetShadowOffset(1, -1)
198 text:Show()
200 return text
203 function QuestHelper:ReleaseText(text)
204 assert(type(text) == "table")
205 for i,t in ipairs(self.free_text) do assert(t ~= text) end
207 for key in pairs(text) do
208 -- Remove all keys except 0, which seems to hold some special data.
209 if key ~= 0 then
210 text[key] = nil
214 text:Hide()
215 text:SetParent(UIParent)
216 text:ClearAllPoints()
217 self.used_text = self.used_text - 1
218 table.insert(self.free_text, text)
221 function QuestHelper:CreateTexture(parent, r, g, b, a)
222 self.used_textures = self.used_textures + 1
223 local tex = table.remove(self.free_textures)
225 if tex then
226 tex:SetParent(parent)
227 else
228 tex = parent:CreateTexture()
231 if not tex:SetTexture(r, g, b, a) and
232 not tex:SetTexture("Interface\\Icons\\Temp.blp") then
233 tex:SetTexture(1, 0, 1, 0.5)
236 tex:ClearAllPoints()
237 tex:SetTexCoord(0, 1, 0, 1)
238 tex:SetVertexColor(1, 1, 1, 1)
239 tex:SetDrawLayer("ARTWORK")
240 tex:SetBlendMode("BLEND")
241 tex:SetWidth(12)
242 tex:SetHeight(12)
243 tex:Show()
245 return tex
248 function QuestHelper:CreateIconTexture(parent, id)
249 local icon = self:CreateTexture(parent, "Interface\\AddOns\\QuestHelper\\Art\\Icons.tga")
251 local w, h = 1/8, 1/8
252 local x, y = ((id-1)%8)*w, math.floor((id-1)/8)*h
254 icon:SetTexCoord(x, x+w, y, y+h)
256 return icon
259 function QuestHelper:CreateDotTexture(parent)
260 local icon = self:CreateIconTexture(parent, 13)
261 icon:SetWidth(5)
262 icon:SetHeight(5)
263 icon:SetVertexColor(0, 0, 0, 0.35)
264 return icon
267 function QuestHelper:CreateGlowTexture(parent)
268 local tex = self:CreateTexture(parent, "Interface\\Addons\\QuestHelper\\Art\\Glow.tga")
270 local angle = math.random()*6.28318530717958647692528676655900576839433879875021164
271 local x, y = math.cos(angle)*0.707106781186547524400844362104849039284835937688474036588339869,
272 math.sin(angle)*0.707106781186547524400844362104849039284835937688474036588339869
274 -- Randomly rotate the texture, so they don't all look the same.
275 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)
276 tex:ClearAllPoints()
278 return tex
281 function QuestHelper:ReleaseTexture(tex)
282 assert(type(tex) == "table")
283 for i,t in ipairs(self.free_textures) do assert(t ~= tex) end
285 for key in pairs(tex) do
286 -- Remove all keys except 0, which seems to hold some special data.
287 if key ~= 0 then
288 tex[key] = nil
292 tex:Hide()
293 tex:SetParent(UIParent)
294 tex:ClearAllPoints()
295 self.used_textures = self.used_textures - 1
296 table.insert(self.free_textures, tex)
299 QuestHelper.recycle_active_cached_tables = {}
300 QuestHelper.recycle_decache_queue = {}
302 function QuestHelper:CacheRegister(obj)
303 if not self.recycle_active_cached_tables[obj] then
304 self.recycle_active_cached_tables[obj] = true
305 table.insert(self.recycle_decache_queue, obj)
309 function QuestHelper:CacheCleanup(obj)
310 local target = self.recycle_decache_queue[1]
312 if not target then return end
313 table.remove(self.recycle_decache_queue, 1)
314 self.recycle_active_cached_tables[target] = nil
316 if target.distance_cache then
317 for k, v in pairs(target.distance_cache) do
318 self:ReleaseTable(v)
320 self:ReleaseTable(target.distance_cache)
321 target.distance_cache = self:CreateTable("objective.distance_cache cleaned")
325 function QuestHelper:DumpCacheData(obj)
326 local caches = 0
327 local cached = 0
328 for k, v in pairs(self.recycle_decache_queue) do
329 caches = caches + 1
330 if v.distance_cache then
331 for q, w in pairs(v.distance_cache) do
332 cached = cached + 1
337 self:TextOut(caches .. " queued caches with a total of " .. cached .. " cached items")