1 QuestHelper_File
["recycle.lua"] = "Development Version"
2 QuestHelper_Loadtime
["recycle.lua"] = GetTime()
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")
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
50 self
.free_tables
[tbl
] = nil
51 setmetatable(tbl
, nil)
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
60 for _
, v
in pairs(self
.recycle_tabletyping
) do
61 freq
[v
] = (freq
[v
] or 0) + 1
65 for k
, v
in pairs(freq
) do
66 table.insert(fqx
, {k
, v
})
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])
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
)
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
96 local release_cycle
= 0
97 function QuestHelper
:ReleaseTable(tbl
)
98 QuestHelper
: Assert(type(tbl
) == "table")
99 QuestHelper
: Assert(not self
.free_tables
[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
110 self
.recycle_tabletyping
[tbl
] = (self
.recycle_tabletyping
[tbl
] or "((unknown))") .. "((released))"
115 function QuestHelper
:RecycleClear()
116 local ct
= QuestHelper
:TableSize(QuestHelper
.free_tables
)
117 QuestHelper
.free_tables
= {}
121 function QuestHelper
:DumpTableTypeFrequencies(silent
)
123 for k
, v
in pairs(self
.recycle_tabletyping
) do
124 freq
[v
] = (freq
[v
] or 0) + 1
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
)
143 function QuestHelper
:CreateFrame(parent
)
144 self
.used_frames
= self
.used_frames
+ 1
145 local frame
= table.remove(self
.free_frames
)
148 frame
:SetParent(parent
)
150 frame
= CreateFrame("Button", string.format("QuestHelperFrame%d",self
.used_frames
), parent
)
153 frame
:SetFrameLevel((parent
or UIParent
):GetFrameLevel()+1)
154 frame
:SetFrameStrata("MEDIUM")
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.
196 for _
, script
in ipairs(frameScripts
) do
197 QH_Hook(frame
, script
, nil)
201 frame
:SetParent(QuestHelper
)
202 frame
:ClearAllPoints()
203 frame
:SetMovable(false)
204 frame
:RegisterForDrag()
205 frame
:RegisterForClicks()
206 frame
:SetBackdrop(nil)
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
)
219 text
:SetParent(parent
)
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)
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.
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
)
260 tex
:SetParent(parent
)
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)
271 tex
:SetTexCoord(0, 1, 0, 1)
272 tex
:SetVertexColor(1, 1, 1, 1)
273 tex
:SetDrawLayer("ARTWORK")
274 tex
:SetBlendMode("BLEND")
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
)
293 function QuestHelper
:CreateDotTexture(parent
)
294 local icon
= self
:CreateIconTexture(parent
, 13)
297 icon
:SetVertexColor(0, 0, 0, 0.35)
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
)
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.
327 tex
:SetParent(UIParent
)
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
354 self
:ReleaseTable(target
.distance_cache
)
355 target
.distance_cache
= self
:CreateTable("objective.distance_cache cleaned")
359 function QuestHelper
:DumpCacheData(obj
)
362 for k
, v
in pairs(self
.recycle_decache_queue
) do
364 if v
.distance_cache
then
365 for q
, w
in pairs(v
.distance_cache
) do
371 self
:TextOut(caches
.. " queued caches with a total of " .. cached
.. " cached items")