summaryrefslogtreecommitdiffstats
path: root/system_cmds/zprint.tproj/zprint.lua
diff options
context:
space:
mode:
Diffstat (limited to 'system_cmds/zprint.tproj/zprint.lua')
-rw-r--r--system_cmds/zprint.tproj/zprint.lua281
1 files changed, 281 insertions, 0 deletions
diff --git a/system_cmds/zprint.tproj/zprint.lua b/system_cmds/zprint.tproj/zprint.lua
new file mode 100644
index 0000000..a5ca245
--- /dev/null
+++ b/system_cmds/zprint.tproj/zprint.lua
@@ -0,0 +1,281 @@
+require 'strict'
+
+-- # zprint
+--
+-- Parse the output of zprint into tables.
+
+local zprint = {}
+
+-- Return the lines inside "dashed" lines -- that is, lines that are entirely
+-- made up of dashes (-) in the string `str`. The `skip_dashed` argument
+-- controls how many dashed lines to skip before returning the lines between it
+-- and the next dashed line.
+local function lines_inside_dashes(str, skip_dashed)
+ local start_pos = 1
+ for _ = 1, skip_dashed do
+ _, start_pos = str:find('\n[-]+\n', start_pos)
+ end
+ assert(start_pos, 'found dashed line in output')
+ local end_pos, _ = str:find('\n[-]+\n', start_pos + 1)
+ assert(end_pos, 'found ending dashed line in output')
+
+ return str:sub(start_pos + 1, end_pos - 1)
+end
+
+-- Iterate through the zones listed in the given zprint(1) output `zpout`.
+--
+-- for zone in zprint_zones(io.stdin:read('*a')) do
+-- print(zone.name, zone.size, zone.used_size)
+-- end
+function zprint.zones(zpout)
+ -- Get to the first section delimited by dashes. This is where the zones are
+ -- recorded.
+ local zones = lines_inside_dashes(zpout, 1)
+
+ -- Create an iterator for each line, for use in our own iteration function.
+ local lines = zones:gmatch('([^\n]+)')
+
+ return function ()
+ -- Grab the next line.
+ local line = lines()
+ if not line then
+ return nil
+ end
+
+ -- Match each column from zprint's output.
+ local name, eltsz, cursz_kb, maxsz_kb, nelts, nelts_max, nused,
+ allocsz_kb = line:match(
+ '([%S]+)%s+(%d+)%s+(%d+)K%s+(%d+)K%s+(%d+)%s+(%d+)%s+(%d+)%s+(%d+)')
+ -- Convert numeric fields to numbers, and into bytes if specified in KiB.
+ eltsz = tonumber(eltsz)
+ local cursz = tonumber(cursz_kb) * 1024
+ local maxsz = tonumber(maxsz_kb) * 1024
+ local usedsz = tonumber(nused) * eltsz
+ local allocsz = tonumber(allocsz_kb) * 1024
+
+ -- Return a table representing the zone.
+ return {
+ name = name, -- the name of the zone
+ size = cursz, -- the size of the zone
+ max_size = maxsz, -- the maximum size of the zone
+ used_size = usedsz, -- the size of all used elements in the zone
+ element_size = eltsz, -- the size of each element in the zone
+ allocation_size = allocsz, -- the size of allocations made for the zone
+ }
+ end
+end
+
+-- Match the output of a vm_tag line
+-- This line has a variable number of columns.
+-- This function returns the name and a table containing each numeric column's
+-- value.
+local function match_tag(line, ncols)
+ -- First try to match names with C++ symbol names.
+ -- These can have whitespace in the argument list.
+ local name_pattern = '^(%S+%b()%S*)'
+ local name = line:match(name_pattern)
+ if not name then
+ name = line:match('(%S+)')
+ if not name then
+ return nil
+ end
+ end
+ local after_name = line:sub(#name)
+ local t = {}
+ for v in line:gmatch('%s+(%d+)K?') do
+ table.insert(t, v)
+ end
+ return name, t
+end
+
+-- Iterate through the tags listed in the given zprint(1) output `zpout`.
+function zprint.tags(zpout)
+ -- Get to the third zone delimited by dashes, where the tags are recorded.
+ local tags = lines_inside_dashes(zpout, 3)
+
+ local lines = tags:gmatch('([^\n]+)')
+
+ return function ()
+ local line = lines()
+ if not line then
+ return nil
+ end
+
+ -- Skip any unloaded kmod lines.
+ while line:match('(unloaded kmod)') do
+ line = lines()
+ end
+ -- End on the zone tags line, since it's not useful.
+ if line:match('zone tags') then
+ return nil
+ end
+
+ local name, matches = match_tag(line)
+ if not name or #matches == 0 then
+ return nil
+ end
+
+ local cursz_kb = matches[#matches]
+ -- If there are fewer than 3 numeric columns, there's no reported peak size
+ local maxsz_kb = nil
+ if #matches > 3 then
+ maxsz_kb = matches[#matches - 1]
+ end
+
+ -- Convert numeric fields to numbers and then into bytes.
+ local cursz = tonumber(cursz_kb) * 1024
+ local maxsz = maxsz_kb and (tonumber(maxsz_kb) * 1024)
+
+ -- Return a table representing the region.
+ return {
+ name = name,
+ size = cursz,
+ max_size = maxsz,
+ }
+ end
+end
+
+-- Iterate through the maps listed in the given zprint(1) output `zpout`.
+function zprint.maps(zpout)
+ local maps = lines_inside_dashes(zpout, 5)
+ local lines = maps:gmatch('([^\n]+)')
+
+ return function()
+ -- Grab the next line.
+ local line = lines()
+ if not line then
+ return nil
+ end
+
+ -- The line can take on 3 different forms. Check for each of them
+
+ -- Check for 3 columns
+ local name, free_kb, largest_free_kb, curr_size_kb = line:match(
+ '(%S+)%s+(%d+)K%s+(%d+)K%s+(%d+)K')
+ local free, largest_free, peak_size_kb, peak_size, size
+ if not name then
+ -- Check for 2 columns
+ name, peak_size_kb, curr_size_kb = line:match('(%S+)%s+(%d+)K%s+(%d+)K')
+ if not name then
+ -- Check for a single column
+ name, curr_size_kb = line:match('(%S+)%s+(%d+)K')
+ assert(name)
+ else
+ peak_size = tonumber(peak_size_kb) * 1024
+ end
+ else
+ free = tonumber(free_kb) * 1024
+ largest_free = tonumber(largest_free_kb) * 1024
+ end
+ size = tonumber(curr_size_kb) * 1024
+
+ return {
+ name = name,
+ size = size,
+ max_size = peak_size,
+ free = free,
+ largest_free = largest_free
+ }
+ end
+end
+
+-- Iterate through the zone views listed in the given zprint(1) output `zpout`.
+function zprint.zone_views(zpout)
+ -- Skip to the zone views
+ local prev_pos = 1
+ -- Look for a line that starts with "zone views" and is followed by a -- line.
+ while true do
+ local start_pos, end_pos = zpout:find('\n[-]+\n', prev_pos)
+ if start_pos == nil then
+ return nil
+ end
+ local before = zpout:sub(prev_pos, start_pos)
+ local zone_views_index = zpout:find('\n%s*zone views%s+[^\n]+\n', prev_pos + 1)
+ prev_pos = end_pos
+ if zone_views_index and zone_views_index < end_pos then
+ break
+ end
+ end
+
+ local zone_views
+ local zone_totals_index = zpout:find("\nZONE TOTALS")
+ if zone_totals_index then
+ zone_views = zpout:sub(prev_pos + 1, zone_totals_index)
+ else
+ zone_views = zpout:sub(prev_pos+ 1)
+ end
+
+ local lines = zone_views:gmatch('([^\n]+)')
+
+ return function()
+ -- Grab the next line.
+ local line = lines()
+ if not line then
+ return nil
+ end
+
+ local name, curr_size_kb = line:match('(%S+)%s+(%d+)')
+ local size = tonumber(curr_size_kb) * 1024
+
+ return {
+ name = name,
+ size = size,
+ }
+ end
+end
+
+function zprint.total(zpout)
+ local total = zpout:match('total[^%d]+(%d+.%d+)M of')
+ local bytes = tonumber(total) * 1024 * 1024
+ return bytes
+end
+
+-- Return a library object, if called from require or dofile.
+local calling_func = debug.getinfo(2).func
+if calling_func == require or calling_func == dofile then
+ return zprint
+end
+
+-- Otherwise, 'recon zprint.lua ...' runs as a script.
+
+local cjson = require 'cjson'
+
+if not arg[1] then
+ io.stderr:write('usage: ', arg[0], ' <zprint-output-path>\n')
+ os.exit(1)
+end
+
+local file
+if arg[1] == '-' then
+ file = io.stdin
+else
+ local err
+ file, err = io.open(arg[1])
+ if not file then
+ io.stderr:write('zprint.lua: ', arg[1], ': open failed: ', err, '\n')
+ os.exit(1)
+ end
+end
+
+local zpout = file:read('all')
+file:close()
+
+local function collect(iter, arg)
+ local tbl = {}
+ for elt in iter(arg) do
+ tbl[#tbl + 1] = elt
+ end
+ return tbl
+end
+
+local zones = collect(zprint.zones, zpout)
+local tags = collect(zprint.tags, zpout)
+local maps = collect(zprint.maps, zpout)
+local zone_views = collect(zprint.zone_views, zpout)
+
+print(cjson.encode({
+ zones = zones,
+ tags = tags,
+ maps = maps,
+ zone_views = zone_views,
+}))