]> git.cameronkatri.com Git - apple_cmds.git/blob - system_cmds/zprint.tproj/zprint.lua
Merge branch 'apple'
[apple_cmds.git] / system_cmds / zprint.tproj / zprint.lua
1 require 'strict'
2
3 -- # zprint
4 --
5 -- Parse the output of zprint into tables.
6
7 local zprint = {}
8
9 -- Return the lines inside "dashed" lines -- that is, lines that are entirely
10 -- made up of dashes (-) in the string `str`. The `skip_dashed` argument
11 -- controls how many dashed lines to skip before returning the lines between it
12 -- and the next dashed line.
13 local function lines_inside_dashes(str, skip_dashed)
14 local start_pos = 1
15 for _ = 1, skip_dashed do
16 _, start_pos = str:find('\n[-]+\n', start_pos)
17 end
18 assert(start_pos, 'found dashed line in output')
19 local end_pos, _ = str:find('\n[-]+\n', start_pos + 1)
20 assert(end_pos, 'found ending dashed line in output')
21
22 return str:sub(start_pos + 1, end_pos - 1)
23 end
24
25 -- Iterate through the zones listed in the given zprint(1) output `zpout`.
26 --
27 -- for zone in zprint_zones(io.stdin:read('*a')) do
28 -- print(zone.name, zone.size, zone.used_size)
29 -- end
30 function zprint.zones(zpout)
31 -- Get to the first section delimited by dashes. This is where the zones are
32 -- recorded.
33 local zones = lines_inside_dashes(zpout, 1)
34
35 -- Create an iterator for each line, for use in our own iteration function.
36 local lines = zones:gmatch('([^\n]+)')
37
38 return function ()
39 -- Grab the next line.
40 local line = lines()
41 if not line then
42 return nil
43 end
44
45 -- Match each column from zprint's output.
46 local name, eltsz, cursz_kb, maxsz_kb, nelts, nelts_max, nused,
47 allocsz_kb = line:match(
48 '([%S]+)%s+(%d+)%s+(%d+)K%s+(%d+)K%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)')
49 -- Convert numeric fields to numbers, and into bytes if specified in KiB.
50 eltsz = tonumber(eltsz)
51 local cursz = tonumber(cursz_kb) * 1024
52 local maxsz = tonumber(maxsz_kb) * 1024
53 local usedsz = tonumber(nused) * eltsz
54 local allocsz = tonumber(allocsz_kb) * 1024
55
56 -- Return a table representing the zone.
57 return {
58 name = name, -- the name of the zone
59 size = cursz, -- the size of the zone
60 max_size = maxsz, -- the maximum size of the zone
61 used_size = usedsz, -- the size of all used elements in the zone
62 element_size = eltsz, -- the size of each element in the zone
63 allocation_size = allocsz, -- the size of allocations made for the zone
64 }
65 end
66 end
67
68 -- Match the output of a vm_tag line
69 -- This line has a variable number of columns.
70 -- This function returns the name and a table containing each numeric column's
71 -- value.
72 local function match_tag(line, ncols)
73 -- First try to match names with C++ symbol names.
74 -- These can have whitespace in the argument list.
75 local name_pattern = '^(%S+%b()%S*)'
76 local name = line:match(name_pattern)
77 if not name then
78 name = line:match('(%S+)')
79 if not name then
80 return nil
81 end
82 end
83 local after_name = line:sub(#name)
84 local t = {}
85 for v in line:gmatch('%s+(%d+)K?') do
86 table.insert(t, v)
87 end
88 return name, t
89 end
90
91 -- Iterate through the tags listed in the given zprint(1) output `zpout`.
92 function zprint.tags(zpout)
93 -- Get to the third zone delimited by dashes, where the tags are recorded.
94 local tags = lines_inside_dashes(zpout, 3)
95
96 local lines = tags:gmatch('([^\n]+)')
97
98 return function ()
99 local line = lines()
100 if not line then
101 return nil
102 end
103
104 -- Skip any unloaded kmod lines.
105 while line:match('(unloaded kmod)') do
106 line = lines()
107 end
108 -- End on the zone tags line, since it's not useful.
109 if line:match('zone tags') then
110 return nil
111 end
112
113 local name, matches = match_tag(line)
114 if not name or #matches == 0 then
115 return nil
116 end
117
118 local cursz_kb = matches[#matches]
119 -- If there are fewer than 3 numeric columns, there's no reported peak size
120 local maxsz_kb = nil
121 if #matches > 3 then
122 maxsz_kb = matches[#matches - 1]
123 end
124
125 -- Convert numeric fields to numbers and then into bytes.
126 local cursz = tonumber(cursz_kb) * 1024
127 local maxsz = maxsz_kb and (tonumber(maxsz_kb) * 1024)
128
129 -- Return a table representing the region.
130 return {
131 name = name,
132 size = cursz,
133 max_size = maxsz,
134 }
135 end
136 end
137
138 -- Iterate through the maps listed in the given zprint(1) output `zpout`.
139 function zprint.maps(zpout)
140 local maps = lines_inside_dashes(zpout, 5)
141 local lines = maps:gmatch('([^\n]+)')
142
143 return function()
144 -- Grab the next line.
145 local line = lines()
146 if not line then
147 return nil
148 end
149
150 -- The line can take on 3 different forms. Check for each of them
151
152 -- Check for 3 columns
153 local name, free_kb, largest_free_kb, curr_size_kb = line:match(
154 '(%S+)%s+(%d+)K%s+(%d+)K%s+(%d+)K')
155 local free, largest_free, peak_size_kb, peak_size, size
156 if not name then
157 -- Check for 2 columns
158 name, peak_size_kb, curr_size_kb = line:match('(%S+)%s+(%d+)K%s+(%d+)K')
159 if not name then
160 -- Check for a single column
161 name, curr_size_kb = line:match('(%S+)%s+(%d+)K')
162 assert(name)
163 else
164 peak_size = tonumber(peak_size_kb) * 1024
165 end
166 else
167 free = tonumber(free_kb) * 1024
168 largest_free = tonumber(largest_free_kb) * 1024
169 end
170 size = tonumber(curr_size_kb) * 1024
171
172 return {
173 name = name,
174 size = size,
175 max_size = peak_size,
176 free = free,
177 largest_free = largest_free
178 }
179 end
180 end
181
182 -- Iterate through the zone views listed in the given zprint(1) output `zpout`.
183 function zprint.zone_views(zpout)
184 -- Skip to the zone views
185 local prev_pos = 1
186 -- Look for a line that starts with "zone views" and is followed by a -- line.
187 while true do
188 local start_pos, end_pos = zpout:find('\n[-]+\n', prev_pos)
189 if start_pos == nil then
190 return nil
191 end
192 local before = zpout:sub(prev_pos, start_pos)
193 local zone_views_index = zpout:find('\n%s*zone views%s+[^\n]+\n', prev_pos + 1)
194 prev_pos = end_pos
195 if zone_views_index and zone_views_index < end_pos then
196 break
197 end
198 end
199
200 local zone_views
201 local zone_totals_index = zpout:find("\nZONE TOTALS")
202 if zone_totals_index then
203 zone_views = zpout:sub(prev_pos + 1, zone_totals_index)
204 else
205 zone_views = zpout:sub(prev_pos+ 1)
206 end
207
208 local lines = zone_views:gmatch('([^\n]+)')
209
210 return function()
211 -- Grab the next line.
212 local line = lines()
213 if not line then
214 return nil
215 end
216
217 local name, curr_size_kb = line:match('(%S+)%s+(%d+)')
218 local size = tonumber(curr_size_kb) * 1024
219
220 return {
221 name = name,
222 size = size,
223 }
224 end
225 end
226
227 function zprint.total(zpout)
228 local total = zpout:match('total[^%d]+(%d+.%d+)M of')
229 local bytes = tonumber(total) * 1024 * 1024
230 return bytes
231 end
232
233 -- Return a library object, if called from require or dofile.
234 local calling_func = debug.getinfo(2).func
235 if calling_func == require or calling_func == dofile then
236 return zprint
237 end
238
239 -- Otherwise, 'recon zprint.lua ...' runs as a script.
240
241 local cjson = require 'cjson'
242
243 if not arg[1] then
244 io.stderr:write('usage: ', arg[0], ' <zprint-output-path>\n')
245 os.exit(1)
246 end
247
248 local file
249 if arg[1] == '-' then
250 file = io.stdin
251 else
252 local err
253 file, err = io.open(arg[1])
254 if not file then
255 io.stderr:write('zprint.lua: ', arg[1], ': open failed: ', err, '\n')
256 os.exit(1)
257 end
258 end
259
260 local zpout = file:read('all')
261 file:close()
262
263 local function collect(iter, arg)
264 local tbl = {}
265 for elt in iter(arg) do
266 tbl[#tbl + 1] = elt
267 end
268 return tbl
269 end
270
271 local zones = collect(zprint.zones, zpout)
272 local tags = collect(zprint.tags, zpout)
273 local maps = collect(zprint.maps, zpout)
274 local zone_views = collect(zprint.zone_views, zpout)
275
276 print(cjson.encode({
277 zones = zones,
278 tags = tags,
279 maps = maps,
280 zone_views = zone_views,
281 }))