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
])
101 for key
in pairs(tbl
) do
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
112 self
.recycle_tabletyping
[tbl
] = (self
.recycle_tabletyping
[tbl
] or "((unknown))") .. "((released))"
117 function QuestHelper
:RecycleClear()
118 local ct
= QuestHelper
:TableSize(QuestHelper
.free_tables
)
119 QuestHelper
.free_tables
= {}
123 function QuestHelper
:DumpTableTypeFrequencies(silent
)
125 for k
, v
in pairs(self
.recycle_tabletyping
) do
126 freq
[v
] = (freq
[v
] or 0) + 1
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
)
145 function QuestHelper
:CreateFrame(parent
)
146 self
.used_frames
= self
.used_frames
+ 1
147 local frame
= table.remove(self
.free_frames
)
150 frame
:SetParent(parent
)
152 frame
= CreateFrame("Button", string.format("QuestHelperFrame%d",self
.used_frames
), parent
)
155 frame
:SetFrameLevel((parent
or UIParent
):GetFrameLevel()+1)
156 frame
:SetFrameStrata("MEDIUM")
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.
198 for i
, script
in ipairs(frameScripts
) do
199 frame
:SetScript(script
, nil)
203 frame
:SetParent(QuestHelper
)
204 frame
:ClearAllPoints()
205 frame
:SetMovable(false)
206 frame
:RegisterForDrag()
207 frame
:RegisterForClicks()
208 frame
:SetBackdrop(nil)
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
)
221 text
:SetParent(parent
)
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)
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.
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
)
262 tex
:SetParent(parent
)
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)
273 tex
:SetTexCoord(0, 1, 0, 1)
274 tex
:SetVertexColor(1, 1, 1, 1)
275 tex
:SetDrawLayer("ARTWORK")
276 tex
:SetBlendMode("BLEND")
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
)
295 function QuestHelper
:CreateDotTexture(parent
)
296 local icon
= self
:CreateIconTexture(parent
, 13)
299 icon
:SetVertexColor(0, 0, 0, 0.35)
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
)
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.
329 tex
:SetParent(UIParent
)
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
356 self
:ReleaseTable(target
.distance_cache
)
357 target
.distance_cache
= self
:CreateTable("objective.distance_cache cleaned")
361 function QuestHelper
:DumpCacheData(obj
)
364 for k
, v
in pairs(self
.recycle_decache_queue
) do
366 if v
.distance_cache
then
367 for q
, w
in pairs(v
.distance_cache
) do
373 self
:TextOut(caches
.. " queued caches with a total of " .. cached
.. " cached items")