From b797e46c0405193a09e4329e387f2006c91da9c2 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Sun, 24 Sep 2023 20:21:41 -0400 Subject: [PATCH 01/16] WIP don't query GPU unless we need to --- src/modules/graphics.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/modules/graphics.lua b/src/modules/graphics.lua index fa5e443..27cfe7c 100644 --- a/src/modules/graphics.lua +++ b/src/modules/graphics.lua @@ -52,8 +52,14 @@ return function(update_freq, config, common, width, point) vid_utilization = 0 } + local runtime_status_file = config.dev_power..'/runtime_status' + + local want_nvidia_query = config.show_temp or config.show_clock + or config.gpu_util or config.mem_util or config.vid_util + local update_state = function() - if i_o.read_file(config.dev_power, nil, '*l') == 'on' then + local is_active = i_o.read_file(runtime_status_file, nil, '*l') == 'active' + if is_active and want_nvidia_query then local nvidia_settings_glob = i_o.execute_cmd(NV_QUERY) if nvidia_settings_glob == nil then mod_state.error = 'Error' @@ -68,6 +74,8 @@ return function(update_freq, config, common, width, point) = __string_match(nvidia_settings_glob, NV_REGEX) mod_state.error = false end + elseif is_active then + mod_state.error = false else mod_state.error = 'Off' end From e7d5b63c388ec32af6cc74fa465f4e6c8a0f3bcd Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Tue, 26 Sep 2023 00:55:39 -0400 Subject: [PATCH 02/16] WIP group processors by threads --- config/config.dhall | 5 +- src/modules/common.lua | 8 ++ src/modules/processor.lua | 143 ++++++++++++++++++---------- src/pure.lua | 26 +++++ src/sys.lua | 193 ++++++++++++++++++++++++++++++-------- 5 files changed, 283 insertions(+), 92 deletions(-) diff --git a/config/config.dhall b/config/config.dhall index 1d8c1b5..1a94aa8 100644 --- a/config/config.dhall +++ b/config/config.dhall @@ -87,10 +87,11 @@ let Memory = let Network = { Type = { geometry : PlotGeo_.Type }, default.geometry = PlotGeo_::{=} } +let CoreGroup = { threads : Natural, rows : Natural, padding : Natural } + let Processor = { Type = - { core_rows : Natural - , core_padding : Natural + { core_groups : List CoreGroup , show_stats : Bool , show_plot : Bool , table_rows : Natural diff --git a/src/modules/common.lua b/src/modules/common.lua index f0d4005..22566b0 100644 --- a/src/modules/common.lua +++ b/src/modules/common.lua @@ -439,6 +439,14 @@ return function(config) ) end + M.make_blank_dial = function(x, y, radius, thickness, threshold) + return dial.make( + geom.make_arc(x, y, radius, DIAL_THETA0, DIAL_THETA1), + arc.config(style.line(thickness, CAP_BUTT), patterns.indicator.bg), + threshold_indicator(threshold) + ) + end + M.make_dial = function(x, y, radius, thickness, threshold, _format, pre_function) return { dial = dial.make( diff --git a/src/modules/processor.lua b/src/modules/processor.lua index b9e979d..9426aa3 100644 --- a/src/modules/processor.lua +++ b/src/modules/processor.lua @@ -1,3 +1,4 @@ +local dial = require 'dial' local compound_dial = require 'compound_dial' local text_table = require 'text_table' local i_o = require 'i_o' @@ -21,7 +22,9 @@ return function(update_freq, main_state, config, common, width, point) ----------------------------------------------------------------------------- -- processor state - local mod_state = cpu.read_cpu_loads(cpu.init_cpu_loads()) + local topology = cpu.get_core_topology() + local mod_state = cpu.read_cpu_loads(cpu.init_cpu_loads(topology)) + local ncpus = cpu.get_cpu_number(topology) local update_state = function() mod_state = cpu.read_cpu_loads(mod_state) @@ -30,35 +33,39 @@ return function(update_freq, main_state, config, common, width, point) ----------------------------------------------------------------------------- -- cores (loads and temps) - local ncpus = cpu.get_cpu_number() - local ncores = cpu.get_core_number() - local nthreads = ncpus / ncores + -- TODO add this back + -- local is_evenly_distributed = function(ncores, rows) + -- if rows == 0 then + -- return false + -- elseif math.fmod(ncores, rows) == 0 then + -- return true + -- else + -- i_o.warnf('could not evenly distribute %i cores over %i rows', ncores, rows) + -- return false + -- end + -- end - local show_cores = false - - if config.core_rows > 0 then - if math.fmod(ncores, config.core_rows) == 0 then - show_cores = true - else - i_o.warnf( - 'could not evenly distribute %i cores over %i rows; disabling', - ncores, - config.core_rows - ) - end - end - - local create_core = function(core_cols, y, c) + local create_core = function(core_cols, y, nthreads, padding, c) local dial_x = point.x + (core_cols == 1 and (width / 2) - or (config.core_padding + dial_outer_radius + - (width - 2 * (dial_outer_radius + config.core_padding)) + or (padding + dial_outer_radius + + (width - 2 * (dial_outer_radius + padding)) * math.fmod(c - 1, core_cols) / (core_cols - 1))) local dial_y = y + dial_outer_radius + (2 * dial_outer_radius + dial_y_spacing) * math.floor((c - 1) / core_cols) - return { + local loads + if nthreads == 1 then + local single_thickness = dial_outer_radius - dial_inner_radius + loads = common.make_blank_dial( + dial_x, + dial_y, + dial_outer_radius - single_thickness / 2, + single_thickness, + 80 + ) + else loads = common.make_compound_dial( dial_x, dial_y, @@ -67,7 +74,10 @@ return function(update_freq, main_state, config, common, width, point) dial_thickness, 80, nthreads - ), + ) + end + return { + loads = loads, coretemp = common.make_text_circle( dial_x, dial_y, @@ -79,44 +89,65 @@ return function(update_freq, main_state, config, common, width, point) } end - local mk_cores = function(y) - local core_cols = ncores / config.core_rows - local cores = pure.map_n(pure.partial(create_core, core_cols, y), ncores) - local coretemp_paths = cpu.get_coretemp_paths() - if #coretemp_paths ~= ncores then - i_o.warnf('could not find all coretemp paths') - end - local update_coretemps = function() - for conky_core_idx, path in pairs(coretemp_paths) do - local temp = __math_floor(0.001 * i_o.read_file(path, nil, '*n')) - common.text_circle_set(cores[conky_core_idx].coretemp, temp) - end - end - local update = function() - for _, load_data in pairs(mod_state) do - compound_dial.set( - cores[load_data.conky_core_idx].loads, - load_data.conky_thread_id, - load_data.percent_active * 100 + local mk_core_group = function(group_config, y) + local nthreads = group_config.threads + local core_topology = topology[nthreads] + local ncores = #core_topology + local core_cols = ncores / group_config.rows + local _create_core = pure.partial( + create_core, core_cols, y, nthreads, group_config.padding + ) + local cores = pure.map_n(_create_core, ncores) + local group_loads = mod_state[nthreads] + local update_loads + local draw_static_loads + local draw_dynamic_loads + if nthreads == 1 then + update_loads = function(c) + dial.set( + cores[c].loads, + group_loads[c][1].percent_active * 100 ) end - update_coretemps() + draw_static_loads = dial.draw_static + draw_dynamic_loads = dial.draw_dynamic + else + update_loads = function(c) + for t = 1, nthreads do + compound_dial.set( + cores[c].loads, + t, + group_loads[c][t].percent_active * 100 + ) + end + end + draw_static_loads = compound_dial.draw_static + draw_dynamic_loads = compound_dial.draw_dynamic + end + local update = function() + for c = 1, ncores do + local temp = __math_floor( + 0.001 * i_o.read_file(core_topology[c].coretemp_path, nil, '*n') + ) + common.text_circle_set(cores[c].coretemp, temp) + update_loads(c) + end end local static = function(cr) - for i = 1, #cores do + for i = 1, ncores do common.text_circle_draw_static(cores[i].coretemp, cr) - compound_dial.draw_static(cores[i].loads, cr) + draw_static_loads(cores[i].loads, cr) end end local dynamic = function(cr) - for i = 1, #cores do + for i = 1, ncores do common.text_circle_draw_dynamic(cores[i].coretemp, cr) - compound_dial.draw_dynamic(cores[i].loads, cr) + draw_dynamic_loads(cores[i].loads, cr) end end return common.mk_acc( width, - (dial_outer_radius * 2 + dial_y_spacing) * config.core_rows + (dial_outer_radius * 2 + dial_y_spacing) * group_config.rows - dial_y_spacing, update, static, @@ -172,8 +203,12 @@ return function(update_freq, main_state, config, common, width, point) ) local update = function() local s = 0 - for i = 1, #mod_state do - s = s + mod_state[i].percent_active + for g = 1, #mod_state do + for c = 1, #mod_state[g] do + for t = 1, #mod_state[g][c] do + s = s + mod_state[g][c][t].percent_active + end + end end common.tagged_percent_timeseries_set(total_load, s / ncpus * 100) end @@ -225,14 +260,18 @@ return function(update_freq, main_state, config, common, width, point) ----------------------------------------------------------------------------- -- main functions + local core_group_section = function (g) + return {pure.partial(mk_core_group, g), true, text_spacing} + end + return { header = 'PROCESSOR', point = point, width = width, set_state = update_state, top = { - {mk_cores, show_cores, text_spacing}, - {mk_hwp_freq, config.show_stats, sep_spacing}, + table.unpack(pure.map(core_group_section, config.core_groups)), + -- {mk_hwp_freq, config.show_stats, sep_spacing}, }, common.mk_section( sep_spacing, diff --git a/src/pure.lua b/src/pure.lua index c7a2866..8532ba9 100644 --- a/src/pure.lua +++ b/src/pure.lua @@ -189,6 +189,32 @@ M.flatten = function(xs) return r end +M.group_with = function(keyfun, valfun, seq) + local f = function(acc, next) + local k = keyfun(next) + local v = valfun(next) + if acc[k] == nil then + acc[k] = {v} + else + acc[k][#acc[k] + 1] = v + end + return acc + end + return M.reduce(f, {}, seq) +end + +M.group_by = function(k, seq) + local f = function(acc, next) + if acc[k] == nil then + acc[k] = {next} + else + acc[k][#acc[k]] = next + end + return acc + end + return M.reduce(f, {}, seq) +end + M.concat = function(...) return M.flatten({...}) end diff --git a/src/sys.lua b/src/sys.lua index 741a356..fa31b46 100644 --- a/src/sys.lua +++ b/src/sys.lua @@ -178,8 +178,15 @@ M.get_core_number = function() return __tonumber(i_o.read_file('/proc/cpuinfo', 'cpu cores%s+:%s(%d+)')) end -M.get_cpu_number = function() - return __tonumber(i_o.execute_cmd('nproc', nil, '*n')) +M.get_cpu_number = function(topology) + local n = 0 + for g = 1, #topology do + for c = 1, #topology[g] do + n = n + #topology[g][c].cpus + end + end + return n + -- return __tonumber(i_o.execute_cmd('nproc', nil, '*n')) end local get_coretemp_dir = function() @@ -188,27 +195,123 @@ local get_coretemp_dir = function() return pure.fmap_maybe(dirname, s) end --- map cores to integer values starting at 1; this is necessary since some cpus --- don't report their core id's as a sequence of integers starting at 0 -local get_core_id_indexer = function() +-- return a table with keys corresponding to physcial core id and values to +-- the number of threads of each core (usually 1 or 2) +M.get_core_threads = function() + local cmd = 'lscpu -y -p=core | grep -v \'^#\' | sort -k1,1n | uniq -c' + local flip = function(c) return {__tonumber(c[2]), __tonumber(c[1])} end local make_indexer = pure.compose( pure.array_to_map, - pure.partial(pure.imap, function(i, c) return {__tonumber(c), i} end), - pure.partial(gmatch_to_table1, '(%d+)') - ) - return pure.fmap_maybe( - make_indexer, - i_o.execute_cmd('lscpu -p=CORE | tail -n+5 | sort | uniq') + pure.partial(pure.map, flip), + pure.partial(gmatch_to_tableN, '(%d+) (%d+)') ) + return pure.fmap_maybe(make_indexer, i_o.execute_cmd(cmd)) end + +-- map cores to integer values starting at 1; this is necessary since some cpus +-- don't report their core id's as a sequence of integers starting at 0 +-- local get_core_id_indexer = function() +-- local make_indexer = pure.compose( +-- pure.array_to_map, +-- pure.partial(pure.imap, function(i, c) return {__tonumber(c), i} end), +-- pure.partial(gmatch_to_table1, '(%d+)') +-- ) +-- return pure.fmap_maybe( +-- make_indexer, +-- i_o.execute_cmd('lscpu -p=CORE | tail -n+5 | sort -k1,1n') +-- ) +-- end + +-- conky_core_idx: the ID of the dial to be drawn for this core +-- conky_thread_idx: the ID of the individual indicator within one dial +-- corresponding to one thread in a core (starting at 1 for each core) + +local get_coretemp_mapper = function() + local d = get_coretemp_dir() + i_o.assert_exe_exists('grep') + local get_labels = pure.compose( + i_o.execute_cmd, + pure.partial(__string_format, 'grep Core %s/temp*_label', true) + ) + local to_tuple = function(m) + return {__tonumber(m[2]), __string_format('%s/%s_input', d, m[1])} + end + local to_map = pure.compose( + pure.array_to_map, + pure.partial(pure.map, to_tuple), + pure.partial(gmatch_to_tableN, '/([^/\n]+)_label:Core (%d+)\n') + ) + return pure.maybe({}, to_map, pure.fmap_maybe(get_labels, d)) +end + +M.get_core_topology = function() + local coretemp_paths = get_coretemp_mapper() + local assign_cpu = function(i, x) + return { + lgl_cpu_id = i, + phy_core_id = __tonumber(x[1]), + phy_cpu_id = __tonumber(x[2]) + } + end + local assign_core = function(acc, next) + local g = acc.grouped + local max_lgl_core_id = #g + local new_phy_core_id = next.phy_core_id + local new_cpu = {phy_cpu_id = next.phy_cpu_id, lgl_cpu_id = next.lgl_cpu_id} + if acc.prev_phy_core_id == new_phy_core_id then + local max_thread = #acc.grouped[max_lgl_core_id].cpus + acc.grouped[max_lgl_core_id].cpus[max_thread + 1] = new_cpu + else + local new_lgl_core_id = max_lgl_core_id + 1 + acc.grouped[new_lgl_core_id] = { + phy_core_id = new_phy_core_id, + lgl_core_id = new_lgl_core_id, + coretemp_path = coretemp_paths[new_phy_core_id], + cpus = {new_cpu} + } + acc.prev_phy_core_id = new_phy_core_id + end + return acc + end + local get_threads = function(x) + return #x.cpus + end + local f = pure.compose( + pure.partial(pure.group_with, get_threads, pure.id), + pure.partial(pure.get, 'grouped'), + pure.partial(pure.reduce, assign_core, {prev_phy_core_id = -1, grouped = {}}), + pure.partial(pure.imap, assign_cpu), + pure.partial(gmatch_to_tableN, '(%d+),(%d+)') + ) + local out = + i_o.execute_cmd('lscpu -y -p=core,cpu | grep -v \'^#\' | sort -k1,1n') + return pure.fmap_maybe(f, out) +end + +-- for t, k in pairs(get_core_topology()) do +-- print(t) +-- for x, y in pairs(k) do +-- print(x, y.phy_core_id, y.coretemp_path, #y.cpus) +-- -- for _, z in pairs(y.cpus) do +-- -- print(x,z.cpu,z.conky_cpu) +-- -- end +-- end +-- end + local get_core_mappings = function() - local ncores = M.get_core_number() + local core_threads = M.get_core_threads() + local assign_cpus = function(x) + return { + cpu_id = __tonumber(x[1]), + core_id = __tonumber(x[2]) + } + end local map_ids = function(indexer) local f = function(acc, next) local cpu_id = __tonumber(next[1]) + 1 - local core_id = next[2] - local conky_core_idx = indexer[__tonumber(core_id)] + local core_id = __tonumber(next[2]) + local conky_core_idx = indexer[core_id] acc.mappings[cpu_id] = { conky_core_idx = conky_core_idx, conky_thread_id = acc.thread_ids[conky_core_idx], @@ -219,12 +322,12 @@ local get_core_mappings = function() local cpu_to_core_map = pure.maybe( {}, pure.partial(gmatch_to_tableN, '(%d+),(%d+)'), - i_o.execute_cmd('lscpu -p=cpu,CORE | tail -n+5') + i_o.execute_cmd('lscpu -y -p=cpu,core | grep -v \'^#\' | sort -k1,1n') ) - local init = {mappings = {}, thread_ids = pure.rep(ncores, 1)} + local init = {mappings = {}, _conky_core_index = 0, _thread_ids = {}} return pure.reduce(f, init, cpu_to_core_map).mappings end - return pure.fmap_maybe(map_ids, get_core_id_indexer()) + -- return pure.fmap_maybe(map_ids, ) end M.get_coretemp_paths = function() @@ -306,35 +409,49 @@ M.read_hwp = function(hwp_paths) return mixed and 'Mixed' or (HWP_MAP[hwp_pref] or 'Unknown') end -M.init_cpu_loads = function() - local m = get_core_mappings() +M.init_cpu_loads = function(topo) + -- -- local m = get_core_mappings() + -- local topo = get_core_topology() local cpu_loads = {} - for cpu_id, core in pairs(m) do - cpu_loads[cpu_id] = { - active_prev = 0, - total_prev = 0, - percent_active = 0, - conky_core_idx = core.conky_core_idx, - conky_thread_id = core.conky_thread_id, - } + for core_group_id, core_group in pairs(topo) do + cpu_loads[core_group_id] = {} + for lgl_core_id, core in pairs(core_group) do + cpu_loads[core_group_id][lgl_core_id] = {} + for thread_id = 1, #core.cpus do + cpu_loads[core_group_id][lgl_core_id][thread_id] = { + active_prev = 0, + total_prev = 0, + percent_active = 0, + -- core_id = lgl_core_id, + -- thread_id = thread_id, + } + end + end end return cpu_loads end M.read_cpu_loads = function(cpu_loads) - local ncpus = #cpu_loads local iter = io.lines('/proc/stat') iter() -- ignore first line - for i = 1, ncpus do - local ln = iter() - local user, system, idle = __string_match(ln, '%d+ (%d+) %d+ (%d+) (%d+)', 4) - local active = user + system - local total = active + idle - local c = cpu_loads[i] - if total > c.total_prev then -- guard against 1/0 errors - c.percent_active = (active - c.active_prev) / (total - c.total_prev) - c.active_prev = active - c.total_prev = total + for group_id = 1, #cpu_loads do + local group = cpu_loads[group_id] + for lgl_core_id = 1, #group do + local core = group[lgl_core_id] + for thread_id = 1, #core do + local ln = iter() + local user, system, idle = + __string_match(ln, '%d+ (%d+) %d+ (%d+) (%d+)', 4) + local active = user + system + local total = active + idle + local thread = core[thread_id] + if total > thread.total_prev then -- guard against 1/0 errors + thread.percent_active = + (active - thread.active_prev) / (total - thread.total_prev) + thread.active_prev = active + thread.total_prev = total + end + end end end return cpu_loads From da9a6b0c4693b24ae94b10a84d0d221ece73cf5e Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Wed, 27 Sep 2023 00:05:11 -0400 Subject: [PATCH 03/16] FIX freq and cpu calculation errors --- src/modules/processor.lua | 89 ++++++++++-------- src/sys.lua | 193 ++++++++++++-------------------------- 2 files changed, 108 insertions(+), 174 deletions(-) diff --git a/src/modules/processor.lua b/src/modules/processor.lua index 9426aa3..0dba35f 100644 --- a/src/modules/processor.lua +++ b/src/modules/processor.lua @@ -6,6 +6,7 @@ local cpu = require 'sys' local pure = require 'pure' local __math_floor = math.floor +local __string_format = string.format return function(update_freq, main_state, config, common, width, point) local dial_inner_radius = 30 @@ -89,6 +90,27 @@ return function(update_freq, main_state, config, common, width, point) } end + local read_load = function(core_topology, phy_core_id, thread_id) + local i = core_topology[phy_core_id].cpus[thread_id].lgl_cpu_id + return mod_state[i].percent_active * 100 + end + + local get_load_functions = function(cores, nthreads, core_topology) + if nthreads == 1 then + local update = function(c) + dial.set(cores[c].loads, read_load(core_topology, c, 1)) + end + return update, dial.draw_static, dial.draw_dynamic + else + local update = function(c) + for t = 1, nthreads do + compound_dial.set(cores[c].loads, t, read_load(core_topology, c, t)) + end + end + return update, compound_dial.draw_static, compound_dial.draw_dynamic + end + end + local mk_core_group = function(group_config, y) local nthreads = group_config.threads local core_topology = topology[nthreads] @@ -98,32 +120,8 @@ return function(update_freq, main_state, config, common, width, point) create_core, core_cols, y, nthreads, group_config.padding ) local cores = pure.map_n(_create_core, ncores) - local group_loads = mod_state[nthreads] - local update_loads - local draw_static_loads - local draw_dynamic_loads - if nthreads == 1 then - update_loads = function(c) - dial.set( - cores[c].loads, - group_loads[c][1].percent_active * 100 - ) - end - draw_static_loads = dial.draw_static - draw_dynamic_loads = dial.draw_dynamic - else - update_loads = function(c) - for t = 1, nthreads do - compound_dial.set( - cores[c].loads, - t, - group_loads[c][t].percent_active * 100 - ) - end - end - draw_static_loads = compound_dial.draw_static - draw_dynamic_loads = compound_dial.draw_dynamic - end + local update_loads, draw_static_loads, draw_dynamic_loads = + get_load_functions(cores, nthreads, core_topology) local update = function() for c = 1, ncores do local temp = __math_floor( @@ -159,13 +157,23 @@ return function(update_freq, main_state, config, common, width, point) -- HWP status local mk_hwp_freq = function(y) - local hwp_paths = cpu.get_hwp_paths() + local hwp_paths = cpu.get_hwp_paths(topology) + local freq_labels + local cpu_group_map = cpu.topology_to_cpu_map(topology) + local format_label = function(group_id) + return __string_format('Ave Freq (%i)', group_id) + end + if #topology == 1 then + freq_labels = {'Ave Freq'} + else + freq_labels = pure.map_n(format_label, #topology) + end local cpu_status = common.make_text_rows( point.x, y, width, text_spacing, - {'HWP Preference', 'Ave Freq'} + pure.concat({'HWP Preference'}, freq_labels) ) local update = function() -- For some reason this call is slow (querying anything with pstate in @@ -174,13 +182,18 @@ return function(update_freq, main_state, config, common, width, point) if main_state.trigger10 == 0 then common.text_rows_set(cpu_status, 1, cpu.read_hwp(hwp_paths)) end - common.text_rows_set(cpu_status, 2, cpu.read_freq()) + local ave_freqs = cpu.read_ave_freqs(topology, cpu_group_map) + local i = 2 + for group_id, _ in pairs(topology) do + common.text_rows_set(cpu_status, i, ave_freqs[group_id]) + i = i + 1 + end end local static = pure.partial(common.text_rows_draw_static, cpu_status) local dynamic = pure.partial(common.text_rows_draw_dynamic, cpu_status) return common.mk_acc( width, - text_spacing, + text_spacing * #topology, update, static, dynamic @@ -203,12 +216,8 @@ return function(update_freq, main_state, config, common, width, point) ) local update = function() local s = 0 - for g = 1, #mod_state do - for c = 1, #mod_state[g] do - for t = 1, #mod_state[g][c] do - s = s + mod_state[g][c][t].percent_active - end - end + for i = 1, ncpus do + s = s + mod_state[i].percent_active end common.tagged_percent_timeseries_set(total_load, s / ncpus * 100) end @@ -269,10 +278,10 @@ return function(update_freq, main_state, config, common, width, point) point = point, width = width, set_state = update_state, - top = { - table.unpack(pure.map(core_group_section, config.core_groups)), - -- {mk_hwp_freq, config.show_stats, sep_spacing}, - }, + top = pure.concat( + pure.map(core_group_section, config.core_groups), + {{mk_hwp_freq, config.show_stats, sep_spacing}} + ), common.mk_section( sep_spacing, {mk_load_plot, config.show_plot, geo.table.sec_break}, diff --git a/src/sys.lua b/src/sys.lua index fa31b46..91cd062 100644 --- a/src/sys.lua +++ b/src/sys.lua @@ -172,7 +172,7 @@ end -------------------------------------------------------------------------------- -- cpu --- ASSUME nproc and lscpu will always be available +-- ASSUME lscpu will always be available M.get_core_number = function() return __tonumber(i_o.read_file('/proc/cpuinfo', 'cpu cores%s+:%s(%d+)')) @@ -180,13 +180,10 @@ end M.get_cpu_number = function(topology) local n = 0 - for g = 1, #topology do - for c = 1, #topology[g] do - n = n + #topology[g][c].cpus - end + for g, c in pairs(topology) do + n = n + g * #c end return n - -- return __tonumber(i_o.execute_cmd('nproc', nil, '*n')) end local get_coretemp_dir = function() @@ -208,25 +205,6 @@ M.get_core_threads = function() return pure.fmap_maybe(make_indexer, i_o.execute_cmd(cmd)) end - --- map cores to integer values starting at 1; this is necessary since some cpus --- don't report their core id's as a sequence of integers starting at 0 --- local get_core_id_indexer = function() --- local make_indexer = pure.compose( --- pure.array_to_map, --- pure.partial(pure.imap, function(i, c) return {__tonumber(c), i} end), --- pure.partial(gmatch_to_table1, '(%d+)') --- ) --- return pure.fmap_maybe( --- make_indexer, --- i_o.execute_cmd('lscpu -p=CORE | tail -n+5 | sort -k1,1n') --- ) --- end - --- conky_core_idx: the ID of the dial to be drawn for this core --- conky_thread_idx: the ID of the individual indicator within one dial --- corresponding to one thread in a core (starting at 1 for each core) - local get_coretemp_mapper = function() local d = get_coretemp_dir() i_o.assert_exe_exists('grep') @@ -289,91 +267,54 @@ M.get_core_topology = function() return pure.fmap_maybe(f, out) end --- for t, k in pairs(get_core_topology()) do --- print(t) --- for x, y in pairs(k) do --- print(x, y.phy_core_id, y.coretemp_path, #y.cpus) --- -- for _, z in pairs(y.cpus) do --- -- print(x,z.cpu,z.conky_cpu) --- -- end --- end --- end - -local get_core_mappings = function() - local core_threads = M.get_core_threads() - local assign_cpus = function(x) - return { - cpu_id = __tonumber(x[1]), - core_id = __tonumber(x[2]) - } - end - local map_ids = function(indexer) - local f = function(acc, next) - local cpu_id = __tonumber(next[1]) + 1 - local core_id = __tonumber(next[2]) - local conky_core_idx = indexer[core_id] - acc.mappings[cpu_id] = { - conky_core_idx = conky_core_idx, - conky_thread_id = acc.thread_ids[conky_core_idx], - } - acc.thread_ids[conky_core_idx] = acc.thread_ids[conky_core_idx] + 1 - return acc +M.topology_to_cpu_map = function(topology) + local r = {} + for group_id, group in pairs(topology) do + for _, core in pairs(group) do + for _, cpu in pairs(core.cpus) do + r[cpu.lgl_cpu_id] = group_id + end end - local cpu_to_core_map = pure.maybe( - {}, - pure.partial(gmatch_to_tableN, '(%d+),(%d+)'), - i_o.execute_cmd('lscpu -y -p=cpu,core | grep -v \'^#\' | sort -k1,1n') - ) - local init = {mappings = {}, _conky_core_index = 0, _thread_ids = {}} - return pure.reduce(f, init, cpu_to_core_map).mappings end - -- return pure.fmap_maybe(map_ids, ) + return r end -M.get_coretemp_paths = function() - local get_paths = function(indexer) - local d = get_coretemp_dir() - i_o.assert_exe_exists('grep') - local get_labels = pure.compose( - i_o.execute_cmd, - pure.partial(__string_format, 'grep Core %s/temp*_label', true) - ) - local to_tuple = function(m) - return { - indexer[__tonumber(m[2])], - __string_format('%s/%s_input', d, m[1]) - } - end - local f = pure.compose( - pure.array_to_map, - pure.partial(pure.map, to_tuple), - pure.partial(gmatch_to_tableN, '/([^/\n]+)_label:Core (%d+)\n') - ) - return pure.maybe({}, f, pure.fmap_maybe(get_labels, d)) - end - return pure.maybe({}, get_paths, get_core_id_indexer()) -end - -local match_freq = function(c) - local f = 0 - local n = 0 - for s in __string_gmatch(c, '(%d+%.%d+)') do - f = f + __tonumber(s) - n = n + 1 - end - return __string_format('%.0f Mhz', f / n) -end - -M.read_freq = function() +M.read_ave_freqs = function(topology, cpu_group_map) -- NOTE: Using the builtin conky functions for getting cpu freq seems to make -- the entire loop jittery due to high variance latency. Querying -- scaling_cur_freq in sysfs seems to do the same thing. It appears lscpu -- (which queries /proc/cpuinfo) is much faster and doesn't have this jittery -- problem. - return pure.maybe('N/A', match_freq, i_o.execute_cmd('lscpu -p=MHZ')) + local out = i_o.execute_cmd('lscpu -p=MHZ') + local init_freqs = function(v) + local r = {} + for group_id, _ in pairs(topology) do + r[group_id] = v + end + return r + end + if out == nil then + return init_freqs('N/A') + else + local ave_freqs = init_freqs(0) + local cpu_id = 1 + for s in __string_gmatch(out, '(%d+%.%d+)') do + local group_id = cpu_group_map[cpu_id] + ave_freqs[group_id] = ave_freqs[group_id] + __tonumber(s) + cpu_id = cpu_id + 1 + end + for group_id, _ in pairs(ave_freqs) do + ave_freqs[group_id] = + __string_format( + '%.0f Mhz', + ave_freqs[group_id] / (group_id * #topology[group_id]) + ) + end + return ave_freqs + end end -M.get_hwp_paths = function() +M.get_hwp_paths = function(topology) -- ASSUME this will never fail return pure.map_n( function(i) @@ -381,7 +322,7 @@ M.get_hwp_paths = function() .. (i - 1) .. '/cpufreq/energy_performance_preference' end, - M.get_cpu_number() + M.get_cpu_number(topology) ) end @@ -410,23 +351,14 @@ M.read_hwp = function(hwp_paths) end M.init_cpu_loads = function(topo) - -- -- local m = get_core_mappings() - -- local topo = get_core_topology() + local ncpus = M.get_cpu_number(topo) local cpu_loads = {} - for core_group_id, core_group in pairs(topo) do - cpu_loads[core_group_id] = {} - for lgl_core_id, core in pairs(core_group) do - cpu_loads[core_group_id][lgl_core_id] = {} - for thread_id = 1, #core.cpus do - cpu_loads[core_group_id][lgl_core_id][thread_id] = { - active_prev = 0, - total_prev = 0, - percent_active = 0, - -- core_id = lgl_core_id, - -- thread_id = thread_id, - } - end - end + for lgl_cpu_id = 1, ncpus do + cpu_loads[lgl_cpu_id] = { + active_prev = 0, + total_prev = 0, + percent_active = 0, + } end return cpu_loads end @@ -434,24 +366,17 @@ end M.read_cpu_loads = function(cpu_loads) local iter = io.lines('/proc/stat') iter() -- ignore first line - for group_id = 1, #cpu_loads do - local group = cpu_loads[group_id] - for lgl_core_id = 1, #group do - local core = group[lgl_core_id] - for thread_id = 1, #core do - local ln = iter() - local user, system, idle = - __string_match(ln, '%d+ (%d+) %d+ (%d+) (%d+)', 4) - local active = user + system - local total = active + idle - local thread = core[thread_id] - if total > thread.total_prev then -- guard against 1/0 errors - thread.percent_active = - (active - thread.active_prev) / (total - thread.total_prev) - thread.active_prev = active - thread.total_prev = total - end - end + for lgl_cpu_id = 1, #cpu_loads do + local ln = iter() + local user, system, idle = + __string_match(ln, '%d+ (%d+) %d+ (%d+) (%d+)', 4) + local active = user + system + local total = active + idle + local cpu = cpu_loads[lgl_cpu_id] + if total > cpu.total_prev then -- guard against 1/0 errors + cpu.percent_active = (active - cpu.active_prev) / (total - cpu.total_prev) + cpu.active_prev = active + cpu.total_prev = total end end return cpu_loads From 98cc789d789cc70031bfff9e23d6f9f9a7b4bcb4 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Wed, 27 Sep 2023 00:40:37 -0400 Subject: [PATCH 04/16] WIP document configuration --- config/config.dhall | 153 +++++++++++++++++- config/fallback.yml | 83 ---------- config/schema.yml | 375 -------------------------------------------- 3 files changed, 146 insertions(+), 465 deletions(-) delete mode 100644 config/fallback.yml delete mode 100644 config/schema.yml diff --git a/config/config.dhall b/config/config.dhall index 1a94aa8..0266149 100644 --- a/config/config.dhall +++ b/config/config.dhall @@ -6,43 +6,94 @@ let Margin = Vector2 Natural let FSPath = { name : Text, path : Text } -let TextGeo = { Type = { text_spacing : Natural }, default.text_spacing = 20 } +let TextGeo = + {- + Defines text dimensions for multiline visuals -let SepGeo = { Type = { sep_spacing : Natural }, default.sep_spacing = 20 } + text_spacing: gap between lines of text + -} + { Type = { text_spacing : Natural }, default.text_spacing = 20 } + +let SepGeo = + {- + Defines separator dimensions + + sep_spacing: gap between the separator and either above or below + -} + { Type = { sep_spacing : Natural }, default.sep_spacing = 20 } let PlotGeo = + {- + Defines plot dimensions + + sec_break: gap between the plot label and the plot itself + height: height of the plot (without the label) + ticks_y: number of ticks on the y axis + -} { Type = { sec_break : Natural, height : Natural, ticks_y : Natural } , default = { sec_break = 20, height = 56, ticks_y = 4 } } let PlotGeo_ = { Type = { plot : PlotGeo.Type }, default.plot = PlotGeo::{=} } -let TableGeo = { Type = { sec_break : Natural }, default.sec_break = 20 } +let TableGeo = + {- + Defines table dimensions + + sec_break: spacing between header and the first table row + -} + { Type = { sec_break : Natural }, default.sec_break = 20 } let TableGeo_ = { Type = { table : TableGeo.Type }, default.table = TableGeo::{=} } let FSGeo = + {- + Defines Filesystem module dimensions + + bar_spacing: spacing between percent usage bars + bar_pad: spacing between usage bars and the left edge of the panel + -} { Type = { bar_spacing : Natural, bar_pad : Natural } //\\ SepGeo.Type , default = { bar_spacing = 20, bar_pad = 100 } /\ SepGeo::{=} } let GfxGeo = + {- + Defines Graphics module dimensions + + See PlotGeo, TextGeo, and SepGeo for options included here + -} { Type = SepGeo.Type //\\ PlotGeo_.Type //\\ TextGeo.Type , default = SepGeo::{=} /\ PlotGeo_::{=} /\ TextGeo::{=} } let MemGeo = + {- + Defines Memory module dimensions + + See PlotGeo, TextGeo, and TableGeo for options included here + -} { Type = TextGeo.Type //\\ PlotGeo_.Type //\\ TableGeo_.Type , default = TextGeo::{=} /\ PlotGeo_::{=} /\ TableGeo_::{=} } let ProcGeo = + {- + Defines Processor module dimensions + + See GfxGeo and TableGeo for options included here + -} { Type = GfxGeo.Type //\\ TableGeo_.Type , default = GfxGeo::{=} /\ TableGeo_::{=} } let PwrGeo = + {- + Defines Processor module dimensions + + See TextGeo and PlotGeo for options included here + -} { Type = TextGeo.Type //\\ PlotGeo_.Type , default = TextGeo::{=} /\ PlotGeo_::{=} } @@ -51,6 +102,14 @@ let AllGeo = { TextGeo, PlotGeo, TableGeo, FSGeo, GfxGeo, MemGeo, ProcGeo, PwrGeo } let FileSystem = + {- + Defines Filesystem module configuration + + show_smart: show SMART daemon indicator + show_seafile: show seafile daemon indicator + fs_paths: list of filesystem paths to monitor for percent usage + geometry: dimensional data for this module + -} { Type = { show_smart : Bool , show_seafile : Bool @@ -61,6 +120,17 @@ let FileSystem = } let Graphics = + {- + Defines Graphics module configuration + + dev_power: show a power indicator + show_temp: show temperature in celsius + show_clock: show clock speed + show_gpu_util: show percent utilization + show_mem_util: show percent memory utilized + show_vid_util: show percent video utilized + geometry: dimensional configuration for this module + -} { Type = { dev_power : Text , show_temp : Bool @@ -74,6 +144,15 @@ let Graphics = } let Memory = + {- + Defines Memory module configuration + + show_stats: show total memory gauge, cache, buffers, shared, and slab + show_plot: show memory utilization plot + show_swap: show swap utilization gauge (only if 'show_stats' is true) + table_rows: top processes by memory to display in table (max 10) + geometry: dimensional configuration for this module + -} { Type = { show_stats : Bool , show_plot : Bool @@ -85,11 +164,33 @@ let Memory = } let Network = + {- + Defines Network module configuration + + geometry: dimensional configuration for this module + -} { Type = { geometry : PlotGeo_.Type }, default.geometry = PlotGeo_::{=} } -let CoreGroup = { threads : Natural, rows : Natural, padding : Natural } +let CoreGroup = + {- + Defines a processor group for the Processor module + + threads: the number of threads for each core in this group (usually 1 or 2) + rows: the number of rows over which to display the cores in this group + padding: the spacing to the left and right of the gauges in this group + -} + { threads : Natural, rows : Natural, padding : Natural } let Processor = + {- + Defines Network module configuration + + core_groups: a list of core groups to display + show_stats: show frequency for each core group and HWP status + show_plot: show percent utilization plot + table_rows: top processes by cpu percent to display in table (max 10) + geometry: dimensional configuration for this module + -} { Type = { core_groups : List CoreGroup , show_stats : Bool @@ -100,23 +201,53 @@ let Processor = , default.geometry = ProcGeo::{=} } -let RaplSpec = { name : Text, address : Text } - let Pacman = + {- + Defines Pacman module configuration + + geometry: dimensional configuration for this module + -} { Type = { geometry : TextGeo.Type }, default.geometry = TextGeo::{=} } +let RaplSpec = + {- + Defines a RAPL endpoint to display in the Power module + + name: nice name to display above plot + address: sysfs address to the directory containing the 'energy_uj' file + -} + { name : Text, address : Text } + let Power = + {- + Defines Power module configuration + + battery: sysfs path to the battery to display + rapl_specs: list of RAPL endpoints to display + geometry: dimensional configuration for this module + -} { Type = { battery : Text, rapl_specs : List RaplSpec, geometry : PwrGeo.Type } , default.geometry = PwrGeo::{=} } let ReadWrite = + {- + Defines ReadWrite module configuration + + geometry: dimensional configuration for this module + -} { Type = { devices : List Text, geometry : PlotGeo_.Type } , default.geometry = PlotGeo_::{=} } -let System = Pacman +let System = + {- + Defines System module configuration + + geometry: dimensional configuration for this module + -} + Pacman let AllModules = { FileSystem @@ -297,6 +428,14 @@ let Patterns = } let Theme = + {- + Defines the theme for displaying the window + + font: the font to use + geometry: the global dimensions to use for each panel and widget (for + everything not define by individual modules) + patterns: colors and gradient definitions + -} { Type = { font : Font.Type , geometry : Geometry.Type diff --git a/config/fallback.yml b/config/fallback.yml deleted file mode 100644 index 3033939..0000000 --- a/config/fallback.yml +++ /dev/null @@ -1,83 +0,0 @@ -bootstrap: - update_interval: 1 - dimensions: [1920, 1080] -modules: - memory: - show_stats: false - show_swap: false - show_plot: true - table_rows: 3 - processor: - core_rows: 0 - core_padding: 0 - show_stats: false - show_plot: true - table_rows: 3 - -layout: - anchor: [12, 11] - panels: - - columns: - - {blocks: [network, 10, memory, 10, processor], width: 436} - margins: [20, 10] - -theme: - font: - family: Neuropolitical - sizes: - normal: 13 - plot_label: 8 - table: 11 - header: 15 - geometry: - plot: - seconds: 90 - ticks: [9, 4] - height: 56 - spacing: 20 - table: - name_chars: 8 - padding: [6, 15] - header_padding: 20 - row_spacing: 13 - header: - underline_offset: 26 - padding: 19 - patterns: - header: 0xefefef - panel: - bg: {color: 0x121212, alpha: 0.7} - text: - active: 0xbfe1ff - inactive: 0xc8c8c8 - critical: 0xff8282 - border: 0x888888 - plot: - grid: 0x666666 - outline: 0x777777 - data: - border: - gradient: - - {stop: 0, color: 0x003f7c} - - {stop: 1, color: 0x1e90ff} - fill: - gradient_alpha: - - {stop: 0.2, color: 0x316ece, alpha: 0.5} - - {stop: 1, color: 0x8cc7ff, alpha: 1.0} - indicator: - bg: - gradient: - - {stop: 0, color: 0x565656} - - {stop: 0.5, color: 0xbfbfbf} - - {stop: 1, color: 0x565656} - fg: - active: - gradient: - - {stop: 0, color: 0x316BA6} - - {stop: 0.5, color: 0x99CEFF} - - {stop: 1, color: 0x316BA6} - critical: - gradient: - - {stop: 0, color: 0xFF3333} - - {stop: 0.5, color: 0xFFB8B8} - - {stop: 1, color: 0xFF3333} diff --git a/config/schema.yml b/config/schema.yml deleted file mode 100644 index 1419fdd..0000000 --- a/config/schema.yml +++ /dev/null @@ -1,375 +0,0 @@ -$schema: "http://json-schema.org/draft-07/schema#" -description: over-engineered conky schema -required: [modules, layout] -additionalProperties: false -properties: - - bootstrap: - required: [update_interval, dimensions] - additionalProperties: false - properties: - update_interval: - description: the update interval (seconds) - type: number - dimensions: - description: the max width/height of the conky window - type: array - minItems: 2 - maxItems: 2 - items: - type: integer - minimum: 1 - - # NOTE none of these are required - modules: - additionalProperties: false - properties: - filesystem: - required: [show_smart, fs_paths] - additionalProperties: false - properties: - show_smart: - description: show the smart deamon indicator - type: boolean - fs_paths: - description: the filesystem paths for which usage should be shown - type: array - minItems: 1 - items: - type: object - required: [name, path] - additionalProperties: false - properties: - name: - type: string - path: - type: string - - graphics: - required: [show_temp, show_clock, show_gpu_util, show_mem_util, show_vid_util] - additionalProperties: false - properties: - dev_power: - description: the sysfs path to the graphics card power indicator - type: string - show_temp: - description: show the GPU temp - type: boolean - show_clock: - description: show the GPU clock speeds - type: boolean - show_gpu_util: - description: show the GPU utilization plot - type: boolean - show_mem_util: - description: show the GPU memory utilization plot - type: boolean - show_vid_util: - description: show the GPU video utilization plot - type: boolean - - memory: - required: [show_stats, show_plot, table_rows] - additionalProperties: false - properties: - show_stats: - description: show memory stats/dial - type: boolean - show_swap: - description: show swap dial - type: boolean - show_plot: - description: show the RAM utilization plot - type: boolean - table_rows: &table - descrition: the number of rows in the table (0 for no table) - type: integer - minimum: 0 - - power: - required: [battery, rapl_specs] - additionalProperties: false - properties: - battery: - description: the battery device to use (or blank if none) - type: string - rapl_specs: - description: the Intel RAPL specs for which plots should be made - type: array - items: - type: object - required: [name, address] - additionalProperties: false - properties: - name: - type: string - address: - type: string - - processor: - required: [core_rows, show_stats, show_plot, table_rows] - additionalProperties: false - properties: - core_rows: - description: the number of rows over which to show discrete cores - type: integer - minimum: 0 - core_padding: - description: horizontal padding to apply to the core layout - type: integer - minimum: 0 - show_stats: - description: show frequency/HWP stats - type: boolean - show_plot: - description: show CPU utilization plot - type: boolean - table_rows: *table - - readwrite: - required: [devices] - additionalProperties: false - properties: - devices: - description: the devices to include in I/O summations - type: array - minItems: 1 - items: - type: string - - layout: - required: [anchor, panels] - additionalProperties: false - properties: - anchor: - description: the coordinates of the upper-left corner to anchor the app - type: array - minItems: 2 - maxItems: 2 - items: - type: integer - panels: - description: either a panel (object) or padding between panels (int) - type: array - items: - anyOf: - - type: integer - minimum: 0 - - type: object - description: layout for a single panel - required: [columns, margins] - additionalProperties: false - properties: - margins: - type: array - minItems: 2 - maxItems: 2 - items: - type: integer - columns: - description: | - either the columns in this panel (object) or padding - between columns (int) - type: array - minItems: 1 - items: - anyOf: - - type: integer - minimum: 0 - - type: object - required: [blocks, width] - additionalProperties: false - properties: - width: - description: the width of all modules in this column - type: integer - minimum: 0 - blocks: - description: | - either a module name (string) or padding (int) - type: array - minItems: 1 - items: - anyOf: - - type: integer - minimum: 0 - - type: string - pattern: - "^system|graphics|processor|readwrite|\ - network|pacman|filesystem|power|memory$" - - theme: - required: [font, geometry, patterns] - additionalProperties: false - properties: - font: - required: [family, sizes] - additionalProperties: false - properties: - family: - type: string - sizes: - required: [normal, plot_label, table, header] - additionalProperties: false - properties: - normal: &font_size - type: integer - minimum: 5 - plot_label: *font_size - table: *font_size - header: *font_size - - geometry: - required: [plot, table] - additionalProperties: false - properties: - plot: - required: [seconds, ticks, height, spacing] - additionalProperties: false - properties: - spacing: - description: the spacing between the label and the plot - type: integer - minimum: 10 - height: - description: the height of the plot - type: integer - minimum: 10 - seconds: - description: the number of seconds on each timeseries plot - type: integer - minimum: 30 - ticks: - description: the number of ticks on the x/y axes - type: array - minItems: 2 - maxItems: 2 - items: - type: integer - minimum: 2 - table: - required: [name_chars, padding, header_padding] - additionalProperties: false - properties: - name_chars: - description: | - the length to which the name column should be trimmed (if any) - type: integer - minimum: 0 - padding: - description: the x/y padding around the table - type: array - minItems: 2 - maxItems: 2 - items: - type: integer - minimum: 0 - header_padding: - description: the padding beneath the column headers - type: integer - minimum: 0 - row_spacing: - description: the distance between the center of each row - type: integer - minimum: 10 - - header: - required: [underline_offset, padding] - additionalProperties: false - properties: - underline_offset: - description: the offset of the underline (from top of header) - type: integer - minimum: 10 - padding: - description: the padding beneath the underline - type: integer - minimum: 0 - - patterns: - required: [header, panel, text, border, plot, indicator] - additionalProperties: false - properties: - header: &pattern - oneOf: - - type: integer - - type: object - oneOf: - - required: [color, alpha] - - required: [gradient] - - required: [gradient_alpha] - properties: - color: - type: integer - maximum: 0xffffff - alpha: &alpha - type: number - minimum: 0 - maximum: 1 - gradient: - type: array - minItems: 2 - items: - type: object - required: [stop, color] - additionalProperties: false - properties: &gradient - stop: - type: number - minimum: 0 - maximum: 1 - color: - type: integer - maximum: 0xffffff - - gradient_alpha: - type: array - minItems: 2 - items: - type: object - required: [stop, color, alpha] - additionalProperties: false - properties: - <<: *gradient - alpha: *alpha - - panel: - required: [bg] - additionalProperties: false - properties: - bg: *pattern - - text: - required: [active, inactive, critical] - additionalProperties: false - properties: - active: *pattern - inactive: *pattern - critical: *pattern - - border: *pattern - - plot: - required: [grid, outline, data] - additionalProperties: false - properties: - grid: *pattern - outline: *pattern - data: - required: [border, fill] - additionalProperties: false - properties: - border: *pattern - fill: *pattern - - indicator: - required: [bg, fg] - additionalProperties: false - properties: - bg: *pattern - fg: - required: [active, critical] - additionalProperties: false - properties: - active: *pattern - critical: *pattern From da5b4d3f2ba173e5ace1a9169f00d571846bfaf4 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Wed, 27 Sep 2023 22:55:13 -0400 Subject: [PATCH 05/16] ADD more docs --- config/config.dhall | 177 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 155 insertions(+), 22 deletions(-) diff --git a/config/config.dhall b/config/config.dhall index 0266149..78747b9 100644 --- a/config/config.dhall +++ b/config/config.dhall @@ -1,3 +1,22 @@ +{- Types to define a conky configuration. + +Key terms: +- widget: the components of a module (dials, bars, plots, etc) +- module: a monitor for some aspect of the system to be displayed +- column: a vertical arrangement of modules +- panel: a horizontal arrangement of columns +- layout: a horizontal arrangement of panels +- geometry: the 'dimensions' of a widget +- pattern: the color/gradient to be used in widgets + +NOTE: geometry is defined on a global level and at the module level. The global +level is defined in the theme type (see below) and applies to all widgets +equally. Some dimensions are not defined globally and are instead delegated to +each module to define themselves (these are not overrides so they must be +specified, although most modules have sensible defaults hardcoded) + +See the 'fallback' config for an example of how to use these types. +-} let Vector2 = \(a : Type) -> { x : a, y : a } let Point = Vector2 Natural @@ -262,6 +281,8 @@ let AllModules = } let ModType = + {- Wrapper type for each module + -} < filesystem : FileSystem.Type | graphics : Graphics.Type | memory : Memory.Type @@ -273,21 +294,61 @@ let ModType = | system : System.Type > -let Annotated = \(a : Type) -> { type : Text, data : a } +let Annotated = + {- Helper to ensure yaml is formatted in a parsable manner for union types -let Block = < Pad : Natural | Mod : Annotated ModType > + The intent is to make a block like: -let Column_ = { blocks : List Block, width : Natural } + parent: + type: + data: + -} + \(a : Type) -> { type : Text, data : a } -let Column = < CPad : Natural | CCol : Column_ > +let Block = + {- Either vertical padding or a module -} + < Pad : Natural | Mod : Annotated ModType > -let Panel_ = { columns : List Column, margins : Margin } +let Column_ = + {- A column of modules to display -let Panel = < PPad : Natural | PPanel : Panel_ > + blocks: a list of blocks (either padding or a module) + width: the width of the column (and consequently everything in said column) + -} + { blocks : List Block, width : Natural } -let Layout = { anchor : Point, panels : List Panel } +let Column = + {- Either a column with modules or padding between columns -} + < CPad : Natural | CCol : Column_ > + +let Panel_ = + {- A panel definition. + + columns: a list of columns to show (must be at least one) + margins: space between the panel border and the columns + -} + { columns : List Column, margins : Margin } + +let Panel = + {- Wrapper type for either a panel or horizontal padding between panels -} + < PPad : Natural | PPanel : Panel_ > + +let Layout = + {- The layout of the display + + 'anchor' is the coordinate of the upper-left corner, and 'panels' contains + the specifications of each panel (ie modules to show). + -} + { anchor : Point, panels : List Panel } let Sizes = + {- The font sizes to use for various widgets + + plot_label: the text above the plot + table: the text in a table (headers and rows) + header: the text of each module header + normal: all other text (excluding plot axes) + -} { Type = { normal : Natural , plot_label : Natural @@ -298,16 +359,29 @@ let Sizes = } let Font = + {- A complete font specification -} { Type = { family : Text, sizes : Sizes.Type } , default = { family = "Neuropolitical", sizes = Sizes::{=} } } let PlotGeometry = + {- Global dimensions for a plot + + seconds: number of seconds to display on the x axis + ticks_x: the number of ticks to display on the x axis + -} { Type = { seconds : Natural, ticks_x : Natural } , default = { seconds = 90, ticks_x = 9 } } let TableGeometry = + {- Global dimensions for a table + + name_chars: the max number of characters a cell can have before being truncated + padding: the margins between the border and the interior text + header_padding: the distance between the header and the first row of text + row_spacing: the distance between each row + -} { Type = { name_chars : Natural , padding : Margin @@ -323,11 +397,17 @@ let TableGeometry = } let HeaderGeometry = + {- Global dimensions for the header of a module + + underline_offset: distance between bottom edge of text and the line below + padding: the distance between the underline and the first widget in the module + -} { Type = { underline_offset : Natural, padding : Natural } , default = { underline_offset = 26, padding = 19 } } let Geometry = + {- Global config for dimensions of various widgets -} { Type = { plot : PlotGeometry.Type , table : TableGeometry.Type @@ -340,28 +420,72 @@ let Geometry = } } -let StopRGB = { color : Natural, stop : Double } +let Color = + {- Alias for a solid color represented as a single hex number -} + Natural -let StopRGBA = { color : Natural, stop : Double, alpha : Double } +let Alpha = + {- Alias for alpha channel as a fraction between 0 and 1 -} + Double -let ColorAlpha = { color : Natural, alpha : Double } +let Stop = + {- Alias for the position of a color in a gradient (between 0 and 1) -} + Double + +let StopRGB = + {- A solid color with a position in a gradiant -} + { color : Color, stop : Stop } + +let StopRGBA = + {- A transparent color with a position in a gradiant -} + { color : Color, stop : Stop, alpha : Alpha } + +let ColorAlpha = + {- A solid transparent color -} + { color : Color, alpha : Alpha } let Pattern = - < RGB : Natural + {- Wrapper for different pattern types -} + < RGB : Color | RGBA : ColorAlpha | GradientRGB : List StopRGB | GradientRGBA : List StopRGBA > let annotatePattern = + {- Helper function to ensure patterns are parsable in yaml + + This will create entries in the yaml config like: + + parent: + type: + date: + : ... + : ... + -} \(a : Pattern) -> { type = showConstructor a, data = a } : Annotated Pattern -let mod = \(a : ModType) -> Block.Mod { type = showConstructor a, data = a } +let mod = + {- Helper function to ensure modules are parsable in yaml + + This will create entries in the yaml config like: + + parent: + type: + date: + : ... + : ... + -} + \(a : ModType) -> Block.Mod { type = showConstructor a, data = a } let APattern = Annotated Pattern let symGradient = + {- Make a symmetric gradient between two colors. + + c0 will be the color of either edge and c1 will be the color in the center + -} \(c0 : Natural) -> \(c1 : Natural) -> annotatePattern @@ -373,6 +497,8 @@ let symGradient = ) let Patterns = + {- All patterns for a given theme + -} { Type = { header : APattern , panel : { bg : APattern } @@ -445,19 +571,26 @@ let Theme = { font = Font::{=}, geometry = Geometry::{=}, patterns = Patterns::{=} } } -let Bootstrap = { update_interval : Natural, dimensions : Point } +let Bootstrap = + {- Defines minimal options to start conky prior to calling any lua code. + -} + { update_interval : Natural, dimensions : Point } -let Config = { bootstrap : Bootstrap, theme : Theme.Type, layout : Layout } +let Config = + {- Global config type -} + { bootstrap : Bootstrap, theme : Theme.Type, layout : Layout } let toConfig = - \(i : Natural) -> - \(x : Natural) -> - \(y : Natural) -> - \(t : Theme.Type) -> - \(l : Layout) -> - { bootstrap = { update_interval = i, dimensions = { x, y } } - , theme = t - , layout = l + {- Helper function to generate a config -} + \(update_interval : Natural) -> + \(width : Natural) -> + \(height : Natural) -> + \(theme : Theme.Type) -> + \(layout : Layout) -> + { bootstrap = + { update_interval, dimensions = { x = width, y = height } } + , theme + , layout } : Config From 564796b81da269fbb1e815990da39de60712e737 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Thu, 28 Sep 2023 17:33:58 -0400 Subject: [PATCH 06/16] FIX processor topology length --- src/modules/processor.lua | 7 ++++--- src/pure.lua | 8 ++++++++ src/sys.lua | 13 ++++++------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/modules/processor.lua b/src/modules/processor.lua index 0dba35f..2bbc05e 100644 --- a/src/modules/processor.lua +++ b/src/modules/processor.lua @@ -160,13 +160,14 @@ return function(update_freq, main_state, config, common, width, point) local hwp_paths = cpu.get_hwp_paths(topology) local freq_labels local cpu_group_map = cpu.topology_to_cpu_map(topology) + local ngroups = pure.length(topology) local format_label = function(group_id) return __string_format('Ave Freq (%i)', group_id) end - if #topology == 1 then + if ngroups == 1 then freq_labels = {'Ave Freq'} else - freq_labels = pure.map_n(format_label, #topology) + freq_labels = pure.map_n(format_label, ngroups) end local cpu_status = common.make_text_rows( point.x, @@ -193,7 +194,7 @@ return function(update_freq, main_state, config, common, width, point) local dynamic = pure.partial(common.text_rows_draw_dynamic, cpu_status) return common.mk_acc( width, - text_spacing * #topology, + text_spacing * ngroups, update, static, dynamic diff --git a/src/pure.lua b/src/pure.lua index 8532ba9..8b05d92 100644 --- a/src/pure.lua +++ b/src/pure.lua @@ -136,6 +136,14 @@ end -------------------------------------------------------------------------------- -- random list things +M.length = function(tbl) + local n = 0 + for _, _ in pairs(tbl) do + n = n + 1 + end + return n +end + -- a stupid but composable function M.get = function(key, tbl) return tbl[key] diff --git a/src/sys.lua b/src/sys.lua index 91cd062..175c945 100644 --- a/src/sys.lua +++ b/src/sys.lua @@ -172,12 +172,6 @@ end -------------------------------------------------------------------------------- -- cpu --- ASSUME lscpu will always be available - -M.get_core_number = function() - return __tonumber(i_o.read_file('/proc/cpuinfo', 'cpu cores%s+:%s(%d+)')) -end - M.get_cpu_number = function(topology) local n = 0 for g, c in pairs(topology) do @@ -195,6 +189,7 @@ end -- return a table with keys corresponding to physcial core id and values to -- the number of threads of each core (usually 1 or 2) M.get_core_threads = function() + i_o.assert_exe_exists('lscpu') local cmd = 'lscpu -y -p=core | grep -v \'^#\' | sort -k1,1n | uniq -c' local flip = function(c) return {__tonumber(c[2]), __tonumber(c[1])} end local make_indexer = pure.compose( @@ -206,8 +201,8 @@ M.get_core_threads = function() end local get_coretemp_mapper = function() - local d = get_coretemp_dir() i_o.assert_exe_exists('grep') + local d = get_coretemp_dir() local get_labels = pure.compose( i_o.execute_cmd, pure.partial(__string_format, 'grep Core %s/temp*_label', true) @@ -224,6 +219,9 @@ local get_coretemp_mapper = function() end M.get_core_topology = function() + i_o.assert_exe_exists('lscpu') + i_o.assert_exe_exists('grep') + i_o.assert_exe_exists('sort') local coretemp_paths = get_coretemp_mapper() local assign_cpu = function(i, x) return { @@ -285,6 +283,7 @@ M.read_ave_freqs = function(topology, cpu_group_map) -- scaling_cur_freq in sysfs seems to do the same thing. It appears lscpu -- (which queries /proc/cpuinfo) is much faster and doesn't have this jittery -- problem. + i_o.assert_exe_exists('lscpu') local out = i_o.execute_cmd('lscpu -p=MHZ') local init_freqs = function(v) local r = {} From b5920374a74605c56ccdf24fc282f3dc76c7564b Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Thu, 28 Sep 2023 21:45:53 -0400 Subject: [PATCH 07/16] WIP support nvidia power management --- src/modules/graphics.lua | 58 +++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/src/modules/graphics.lua b/src/modules/graphics.lua index 27cfe7c..1a5786a 100644 --- a/src/modules/graphics.lua +++ b/src/modules/graphics.lua @@ -3,7 +3,7 @@ local i_o = require 'i_o' return function(update_freq, config, common, width, point) local NA = 'N/A' - local NVIDIA_EXE = 'nvidia-settings' + local NVIDIA_EXE = 'nvidia-smi' local geo = config.geometry local sep_spacing = geo.sep_spacing @@ -22,29 +22,17 @@ return function(update_freq, config, common, width, point) -- vars to process the nv settings glob -- - -- glob will be of the form: - -- - -- - -- - -- , - -- graphics=, memory=, video=, PCIe= - local NV_QUERY = NVIDIA_EXE.. - ' -t'.. - ' -q UsedDedicatedGPUmemory'.. - ' -q TotalDedicatedGPUmemory'.. - ' -q ThermalSensorReading'.. - ' -q [gpu:0]/GPUCurrentClockFreqs'.. - ' -q [gpu:0]/GPUutilization'.. - ' 2>/dev/null' - local NV_REGEX = '(%d+)\n'.. - '(%d+)\n'.. - '(%d+)\n'.. - '(%d+),(%d+)\n'.. - 'graphics=(%d+), memory=%d+, video=(%d+), PCIe=%d+\n' + local NV_QUERY = NVIDIA_EXE.. + ' --query-gpu=memory.used,memory.total,temperature.gpu,clocks.gr,clocks.mem,utilization.gpu,utilization.decoder'.. + ' --format=csv,noheader,nounits' + + local NV_REGEX = '(%d+), (%d+), (%d+), (%d+), (%d+), (%d+), (%d+)' local mod_state = { error = false, + gpu_frequency = 0, + memory_frequency = 0, used_memory = 0, total_memory = 0, temp_reading = 0, @@ -52,6 +40,11 @@ return function(update_freq, config, common, width, point) vid_utilization = 0 } + local sleep_token = 0 + local sleep_limit = 10 + local gpu_idle_freq_limit = 250 + + -- TODO ensure this file exists local runtime_status_file = config.dev_power..'/runtime_status' local want_nvidia_query = config.show_temp or config.show_clock @@ -59,7 +52,28 @@ return function(update_freq, config, common, width, point) local update_state = function() local is_active = i_o.read_file(runtime_status_file, nil, '*l') == 'active' - if is_active and want_nvidia_query then + -- this will make the nvidia-smi query fire only so often when the clock + -- is below a certain threshold. This is necessary to get the GPU to + -- suspend when nothing is 'using' it, at the cost of lowering the + -- response time for when it eventually is used again. Maybe won't + -- matter that much since the jobs that use the GPU tend to be long + -- anyways, so a few seconds won't hurt. Furthermore, there are ways to + -- wake this up manually by detecting certain processes the likely will + -- use the GPU (ffmpeg and friends) or detecting processes that are + -- holding /dev/nvidia* files (which isn't foolproof but it will capture + -- most events) + if is_active and + mod_state.gpu_frequency > 0 and + mod_state.gpu_frequency < gpu_idle_freq_limit then + if sleep_token < sleep_limit - 1 then + sleep_token = sleep_token + 1 + else + sleep_token = 0 + end + else + sleep_token = 0 + end + if is_active and want_nvidia_query and sleep_token == 0 then local nvidia_settings_glob = i_o.execute_cmd(NV_QUERY) if nvidia_settings_glob == nil then mod_state.error = 'Error' @@ -72,11 +86,13 @@ return function(update_freq, config, common, width, point) mod_state.gpu_utilization, mod_state.vid_utilization = __string_match(nvidia_settings_glob, NV_REGEX) + mod_state.gpu_frequency = tonumber(mod_state.gpu_frequency) mod_state.error = false end elseif is_active then mod_state.error = false else + mod_state.gpu_frequency = 0 mod_state.error = 'Off' end end From 1ab23d039ba71f044ce6c5ca4d5ce7808258c859 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Thu, 28 Sep 2023 23:25:11 -0400 Subject: [PATCH 08/16] ENH make gpu power limits configurable --- config/config.dhall | 20 +++++++++++++++- src/modules/graphics.lua | 40 ++++++++++++++++++++++--------- src/pure.lua | 52 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 12 deletions(-) diff --git a/config/config.dhall b/config/config.dhall index 78747b9..246c398 100644 --- a/config/config.dhall +++ b/config/config.dhall @@ -138,6 +138,23 @@ let FileSystem = , default.geometry = FSGeo::{=} } +let GPUPower = + {- + Controls how the GPU will be queries so it can go to sleep when 'idle'. + + freq_limit: only query the GPU every 'cycle_seconds' when frequency is below + this value; this should be a sensible value representing and + 'idle' gpu state. + cycle_seconds: the number of seconds to query the GPU when below the idle + frequency limit; note that this must be long enough to let + the GPU go to sleep when it actually is ready to sleep, but + not so long that the next query will miss too much activity + in the case the GPU doesn't shut off + -} + { Type = { freq_limit : Natural, cycle_seconds : Natural } + , default = { freq_limit = 250, cycle_seconds = 10 } + } + let Graphics = {- Defines Graphics module configuration @@ -158,8 +175,9 @@ let Graphics = , show_mem_util : Bool , show_vid_util : Bool , geometry : GfxGeo.Type + , power : GPUPower.Type } - , default.geometry = GfxGeo::{=} + , default = { geometry = GfxGeo::{=}, power = GPUPower::{=} } } let Memory = diff --git a/src/modules/graphics.lua b/src/modules/graphics.lua index 1a5786a..eca0c9e 100644 --- a/src/modules/graphics.lua +++ b/src/modules/graphics.lua @@ -20,29 +20,43 @@ return function(update_freq, config, common, width, point) i_o.assert_exe_exists(NVIDIA_EXE) - -- vars to process the nv settings glob - -- + local mk_query_cmd = function(props) + return string.format( + '%s --query-gpu=%s --format=csv,noheader,nounits', + NVIDIA_EXE, + pure.collapse(props, ',') + ) + end - local NV_QUERY = NVIDIA_EXE.. - ' --query-gpu=memory.used,memory.total,temperature.gpu,clocks.gr,clocks.mem,utilization.gpu,utilization.decoder'.. - ' --format=csv,noheader,nounits' + -- TODO also use encoder for video util? + -- TODO add video clock speed? + local query_stats = mk_query_cmd( + { + 'memory.used', + 'temperature.gpu', + 'clocks.gr', + 'clocks.mem', + 'utilization.gpu', + 'utilization.decoder' + } + ) - local NV_REGEX = '(%d+), (%d+), (%d+), (%d+), (%d+), (%d+), (%d+)' + local NV_REGEX = '(%d+), (%d+), (%d+), (%d+), (%d+), (%d+)' local mod_state = { error = false, + total_memory = 0, gpu_frequency = 0, memory_frequency = 0, used_memory = 0, - total_memory = 0, temp_reading = 0, gpu_utilization = 0, vid_utilization = 0 } local sleep_token = 0 - local sleep_limit = 10 - local gpu_idle_freq_limit = 250 + local sleep_limit = config.power.cycle_seconds + local gpu_idle_freq_limit = config.power.freq_limit -- TODO ensure this file exists local runtime_status_file = config.dev_power..'/runtime_status' @@ -74,12 +88,11 @@ return function(update_freq, config, common, width, point) sleep_token = 0 end if is_active and want_nvidia_query and sleep_token == 0 then - local nvidia_settings_glob = i_o.execute_cmd(NV_QUERY) + local nvidia_settings_glob = i_o.execute_cmd(query_stats) if nvidia_settings_glob == nil then mod_state.error = 'Error' else mod_state.used_memory, - mod_state.total_memory, mod_state.temp_reading, mod_state.gpu_frequency, mod_state.memory_frequency, @@ -89,6 +102,11 @@ return function(update_freq, config, common, width, point) mod_state.gpu_frequency = tonumber(mod_state.gpu_frequency) mod_state.error = false end + -- query total memory if we need it (do this here so we don't turn on + -- the gpu every time we start conky) + if mod_state.total_memory == 0 and config.show_mem_util then + mod_state.total_memory = tonumber(i_o.execute_cmd(mk_query_cmd({'memory.total'}))) + end elseif is_active then mod_state.error = false else diff --git a/src/pure.lua b/src/pure.lua index 8b05d92..6485ebd 100644 --- a/src/pure.lua +++ b/src/pure.lua @@ -4,6 +4,7 @@ local err = require 'err' local __math_floor = math.floor local __table_insert = table.insert +local __string_format = string.format -------------------------------------------------------------------------------- -- zippy functions @@ -50,9 +51,52 @@ M.reduce = function(f, init, seq) end end +M.member = function(x, seq) + return M.reduce( + function(acc, next) return acc or next == x end, + false, + M.values(seq) + ) +end + +M.collapse = function(seq, delim) + if #seq == 0 then + return '' + elseif #seq == 1 then + return seq[1] + else + local init, rest = M.head(seq) + return M.reduce( + function(acc, next) return __string_format('%s%s%s', acc, delim, next) end, + init, + rest + ) + end +end + -------------------------------------------------------------------------------- -- mappy functions +M.keys = function(seq) + local r = {} + local n = 1 + for k, _ in pairs(seq) do + r[n] = k + n = n + 1 + end + return r +end + +M.values = function(seq) + local r = {} + local n = 1 + for _, v in pairs(seq) do + r[n] = v + n = n + 1 + end + return r +end + M.map = function(f, seq, ...) local r = {} for i = 1, #seq do @@ -144,6 +188,14 @@ M.length = function(tbl) return n end +M.head = function(seq) + local r = {} + for i = 2, #seq do + r[i - 1] = seq[i] + end + return seq[1], r +end + -- a stupid but composable function M.get = function(key, tbl) return tbl[key] From cc014824773942ce9e90bc7d017592fd8f07a8f0 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Thu, 28 Sep 2023 23:26:58 -0400 Subject: [PATCH 09/16] ENH clarify docs --- config/config.dhall | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.dhall b/config/config.dhall index 246c398..884bcd1 100644 --- a/config/config.dhall +++ b/config/config.dhall @@ -144,7 +144,7 @@ let GPUPower = freq_limit: only query the GPU every 'cycle_seconds' when frequency is below this value; this should be a sensible value representing and - 'idle' gpu state. + 'idle' gpu state. Set to 0 to disable this check entirely. cycle_seconds: the number of seconds to query the GPU when below the idle frequency limit; note that this must be long enough to let the GPU go to sleep when it actually is ready to sleep, but From 187a14863102ace870602dc039333ce85bc168e5 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Thu, 28 Sep 2023 23:27:46 -0400 Subject: [PATCH 10/16] ENH use locals --- src/modules/graphics.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/graphics.lua b/src/modules/graphics.lua index eca0c9e..87e8d4f 100644 --- a/src/modules/graphics.lua +++ b/src/modules/graphics.lua @@ -21,7 +21,7 @@ return function(update_freq, config, common, width, point) i_o.assert_exe_exists(NVIDIA_EXE) local mk_query_cmd = function(props) - return string.format( + return __string_format( '%s --query-gpu=%s --format=csv,noheader,nounits', NVIDIA_EXE, pure.collapse(props, ',') @@ -99,13 +99,13 @@ return function(update_freq, config, common, width, point) mod_state.gpu_utilization, mod_state.vid_utilization = __string_match(nvidia_settings_glob, NV_REGEX) - mod_state.gpu_frequency = tonumber(mod_state.gpu_frequency) + mod_state.gpu_frequency = __tonumber(mod_state.gpu_frequency) mod_state.error = false end -- query total memory if we need it (do this here so we don't turn on -- the gpu every time we start conky) if mod_state.total_memory == 0 and config.show_mem_util then - mod_state.total_memory = tonumber(i_o.execute_cmd(mk_query_cmd({'memory.total'}))) + mod_state.total_memory = __tonumber(i_o.execute_cmd(mk_query_cmd({'memory.total'}))) end elseif is_active then mod_state.error = false From 500d8b634e3c53630f99fa13955d3af9c9c5d8c4 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Fri, 29 Sep 2023 00:32:51 -0400 Subject: [PATCH 11/16] WIP add validation to config --- config/config.dhall | 2 +- conky.conf | 5 +- src/validate.lua | 224 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 src/validate.lua diff --git a/config/config.dhall b/config/config.dhall index 884bcd1..4a82b6f 100644 --- a/config/config.dhall +++ b/config/config.dhall @@ -159,7 +159,7 @@ let Graphics = {- Defines Graphics module configuration - dev_power: show a power indicator + dev_power: sysfs path to graphics card power directory show_temp: show temperature in celsius show_clock: show clock speed show_gpu_util: show percent utilization diff --git a/conky.conf b/conky.conf index f8bca59..559e6da 100644 --- a/conky.conf +++ b/conky.conf @@ -54,6 +54,7 @@ package.cpath = conky_dir..'lib/lib/lua/5.4/?.so;' local yaml = require 'lyaml' local i_o = require 'i_o' +local validate = require 'validate' local config_path = '/tmp/conky.yml' @@ -68,7 +69,9 @@ local find_valid_config = function(paths) local rc = try_read_config(path) if rc == 0 then i_o.infof('Using config at %s', path) - return yaml.load(i_o.read_file(config_path)) + local config = yaml.load(i_o.read_file(config_path)) + validate.validate_config(config) + return config else i_o.warnf('could not read %s; trying next', path) end diff --git a/src/validate.lua b/src/validate.lua new file mode 100644 index 0000000..1c1d6be --- /dev/null +++ b/src/validate.lua @@ -0,0 +1,224 @@ +local M = {} + +local err = require 'err' +local i_o = require 'i_o' +local pure = require 'pure' + +M.assert_non_nil = function(what, x) + i_o.assertf(x ~= nil, '%s: %f must not be nil', what, x) +end + +M.assert_number = function(what, x) + i_o.assertf(err.get_type(x) == 'number', '%s: \'%s\' must be a number', what, x) +end + +M.assert_integer = function(what, x) + M.assert_number(what, x) + i_o.assertf(math.fmod(x, 1) == 0, '%s: \'%s\' must be an integer', what, x) +end + +M.assert_less_than = function(what, x, upper) + M.assert_number(what, x) + i_o.assertf( + x < upper, + '%s: %f must be less than %f', + what, x, upper + ) +end + +M.assert_greater_than = function(what, x, lower) + M.assert_number(what, x) + i_o.assertf( + x > lower, + '%s: %f must be greater than %f', + what, x, lower + ) +end + +M.assert_less_than_or_eq = function(what, x, upper) + M.assert_number(what, x) + i_o.assertf( + x <= upper, + '%s: %f must be less than or equal to %f', + what, x, upper + ) +end + +M.assert_greater_than_or_eq = function(what, x, lower) + M.assert_number(what, x) + i_o.assertf( + x >= lower, + '%s: %f must be greater than or equal to %f', + what, x, lower + ) +end + +M.assert_positive = function(what, x) + M.assert_greater_than(what, x, 0) +end + +M.assert_between = function(what, x, lower, upper) + M.assert_number(what, x) + i_o.assertf( + x >= lower and x <= upper, + '%s: %f must be between %f and %f', + what, x, lower, upper + ) +end + +M.assert_member = function(what, x, seq) + M.assert_non_nil(what, x) + local xs = pure.collapse(pure.keys(seq), ', ') + i_o.assertf(pure.member(x, seq), '%s: %s is not one of %s', what, x, xs) +end + +M.assert_unit_fraction = function(what, x) + M.assert_number(what, x) + M.assert_between(what, x, 0, 1) +end + +M.assert_color = function(what, x) + M.assert_number(what, x) + M.assert_between(what, x, 0, 0xffffff) +end + +M.assert_color_alpha = function(what, x) + M.assert_color(what..'.color', x.color) + M.assert_unit_fraction(what..'.alpha', x.alpha) +end + +M.assert_color_stop = function(what, x) + M.assert_color(what..'.color', x.color) + M.assert_unit_fraction(what..'.stop', x.stop) +end + +M.assert_color_stop_alpha = function(what, x) + M.assert_color(what..'.color', x.color) + M.assert_unit_fraction(what..'.stop', x.stop) + M.assert_unit_fraction(what..'.alpha', x.alpha) +end + +M.assert_valid_pattern = function(what, pat) + local t = pat.type + if t == "RGB" then + M.assert_color(what, pat.data) + elseif t == "RGBA" then + M.assert_color_alpha(what, pat.data) + elseif t == "GradientRGB" then + for _, x in pairs(pat.data) do + M.assert_color_stop(what, x) + end + elseif t == "GradientRGBA" then + for _, x in pairs(pat.data) do + M.assert_color_stop_alpha(what, x) + end + else + i_o.assertf(nil, '%s: pattern has invalid type \'%s\'', what, t) + end +end + +-- TODO make spacing parameters aware of thickness, right now they start at the +-- centry of lines, circles, etc. + +local validate_filesystem = function(x, width) + -- TODO ensure paths exist here + M.assert_greater_than('filesystem.bar_spacing', x.geometry.bar_spacing, 10) + M.assert_between('filesystem.bar_pad', x.geometry.bar_pad, 20, width) +end + +local validate_graphics = function(x) +end + +local validate_memory = function(x) +end + +local validate_network = function(x) +end + +local validate_pacman = function(x) +end + +local validate_power = function(x) +end + +local validate_processor = function(x) +end + +local validate_readwrite = function(x) +end + +local validate_system = function(x) +end + +M.validate_config = function(config) + local boot = config.bootstrap + M.assert_positive('bootstrap.update_interval', boot.update_interval) + M.assert_positive('bootstrap.dimensions.x', boot.dimensions.x) + M.assert_positive('bootstrap.dimensions.y', boot.dimensions.y) + + local font = config.theme.font.sizes + local _font = 'theme.font.sizes' + M.assert_positive(_font..'normal', font.normal) + M.assert_positive(_font..'plot_label', font.plot_label) + M.assert_positive(_font..'table', font.table) + M.assert_positive(_font..'header', font.header) + + local geo = config.theme.geometry + local _geo = 'theme.geometry.' + M.assert_greater_than(_geo..'plot.seconds', geo.plot.seconds, 10) + M.assert_greater_than(_geo..'plot.ticks_x', geo.plot.ticks_x, 4) + M.assert_greater_than(_geo..'table.name_chars', geo.table.name_chars, 5) + M.assert_greater_than(_geo..'table.row_spacing', geo.table.row_spacing, 10) + + local pat = config.theme.patterns + local _pat = 'theme.pattern.' + M.assert_valid_pattern(_pat..'header', pat.header) + M.assert_valid_pattern(_pat..'panel.bg', pat.panel.bg) + M.assert_valid_pattern(_pat..'text.active', pat.text.active) + M.assert_valid_pattern(_pat..'text.inactive', pat.text.inactive) + M.assert_valid_pattern(_pat..'text.critical', pat.text.critical) + M.assert_valid_pattern(_pat..'border', pat.border) + M.assert_valid_pattern(_pat..'plot.grid', pat.plot.grid) + M.assert_valid_pattern(_pat..'plot.outline', pat.plot.outline) + M.assert_valid_pattern(_pat..'plot.data.border', pat.plot.data.border) + M.assert_valid_pattern(_pat..'plot.data.fill', pat.plot.data.fill) + M.assert_valid_pattern(_pat..'indicator.bg', pat.indicator.bg) + M.assert_valid_pattern(_pat..'indicator.fg.active', pat.indicator.fg.active) + M.assert_valid_pattern(_pat..'indicator.fg.critical', pat.indicator.fg.critical) + + for _, panel in pairs(config.layout.panels) do + if type(panel) == "table" then + for _, column in pairs(panel.columns) do + if type(column) == "table" then + for _, block in pairs(column.blocks) do + if type(block) == "table" then + local t = block.type + local d = block.data + if t == "filesystem" then + validate_filesystem(d, column.width) + elseif t == "graphics" then + validate_graphics(d) + elseif t == "memory" then + validate_memory(d) + elseif t == "network" then + validate_network(d) + elseif t == "pacman" then + validate_pacman(d) + elseif t == "power" then + validate_power(d) + elseif t == "processor" then + validate_processor(d) + elseif t == "readwrite" then + validate_readwrite(d) + elseif t == "system" then + validate_system(d) + end + end + end + end + end + end + end +end + +return M From d82ad44f58dade79a366add01e8a1e177f2cb2e8 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Sat, 30 Sep 2023 00:37:23 -0400 Subject: [PATCH 12/16] ADD config validation --- src/modules/graphics.lua | 4 +-- src/modules/processor.lua | 25 +++++++++--------- src/sys.lua | 8 +++--- src/validate.lua | 42 +++++++++++++++++++++++++------ src/widget/text/text_internal.lua | 1 + 5 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/modules/graphics.lua b/src/modules/graphics.lua index 87e8d4f..55dce11 100644 --- a/src/modules/graphics.lua +++ b/src/modules/graphics.lua @@ -1,5 +1,5 @@ -local pure = require 'pure' -local i_o = require 'i_o' +local pure = require 'pure' +local i_o = require 'i_o' return function(update_freq, config, common, width, point) local NA = 'N/A' diff --git a/src/modules/processor.lua b/src/modules/processor.lua index 2bbc05e..d1d0d7e 100644 --- a/src/modules/processor.lua +++ b/src/modules/processor.lua @@ -31,21 +31,22 @@ return function(update_freq, main_state, config, common, width, point) mod_state = cpu.read_cpu_loads(mod_state) end + -- test that the core groups we want are actually displayable + for _, g in pairs(config.core_groups) do + i_o.assertf( + topology[g.threads] ~= nil, + 'processor: no group with %s threads', 1 + ) + local ncores = #topology[g.threads] + i_o.assertf( + math.fmod(ncores, g.rows) == 0, + 'processor: could not evenly divide %s cores into %s rows', ncores, g.rows + ) + end + ----------------------------------------------------------------------------- -- cores (loads and temps) - -- TODO add this back - -- local is_evenly_distributed = function(ncores, rows) - -- if rows == 0 then - -- return false - -- elseif math.fmod(ncores, rows) == 0 then - -- return true - -- else - -- i_o.warnf('could not evenly distribute %i cores over %i rows', ncores, rows) - -- return false - -- end - -- end - local create_core = function(core_cols, y, nthreads, padding, c) local dial_x = point.x + (core_cols == 1 diff --git a/src/sys.lua b/src/sys.lua index 175c945..d83b0a3 100644 --- a/src/sys.lua +++ b/src/sys.lua @@ -88,10 +88,10 @@ end -------------------------------------------------------------------------------- -- intel powercap -local SYSFS_RAPL = '/sys/class/powercap' +M.SYSFS_RAPL = '/sys/class/powercap' M.intel_powercap_reader = function(dev) - local uj = __string_format('%s/%s/energy_uj', SYSFS_RAPL, dev) + local uj = __string_format('%s/%s/energy_uj', M.SYSFS_RAPL, dev) i_o.assert_file_readable(uj) return function() return read_micro(uj) end end @@ -99,10 +99,10 @@ end -------------------------------------------------------------------------------- -- battery -local SYSFS_POWER = '/sys/class/power_supply' +M.SYSFS_POWER = '/sys/class/power_supply' local format_power_path = function(battery, property) - local p = __string_format('%s/%s/%s', SYSFS_POWER, battery, property) + local p = __string_format('%s/%s/%s', M.SYSFS_POWER, battery, property) i_o.assert_file_readable(p) return p end diff --git a/src/validate.lua b/src/validate.lua index 1c1d6be..e5547e8 100644 --- a/src/validate.lua +++ b/src/validate.lua @@ -3,6 +3,7 @@ local M = {} local err = require 'err' local i_o = require 'i_o' local pure = require 'pure' +local sys = require 'sys' M.assert_non_nil = function(what, x) i_o.assertf(x ~= nil, '%s: %f must not be nil', what, x) @@ -98,6 +99,15 @@ M.assert_color_stop_alpha = function(what, x) M.assert_unit_fraction(what..'.alpha', x.alpha) end +M.assert_file_exists = function(what, path) + i_o.assertf(i_o.file_exists(path), '%s: %s does not exist', what, path) +end + +M.assert_file_readable = function(what, path) + M.assert_file_exists(what, path) + i_o.assertf(i_o.file_readable(path), '%s: %s is not readable', what, path) +end + M.assert_valid_pattern = function(what, pat) local t = pat.type if t == "RGB" then @@ -120,34 +130,52 @@ end -- TODO make spacing parameters aware of thickness, right now they start at the -- centry of lines, circles, etc. -local validate_filesystem = function(x, width) - -- TODO ensure paths exist here - M.assert_greater_than('filesystem.bar_spacing', x.geometry.bar_spacing, 10) - M.assert_between('filesystem.bar_pad', x.geometry.bar_pad, 20, width) +local validate_filesystem = function(x) + for _, f in pairs(x.fs_paths) do + M.assert_file_exists('filesystem.fs_paths', f.path) + end end local validate_graphics = function(x) + M.assert_file_readable('graphics.dev_power', x.dev_power) + M.assert_greater_than('graphics.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2) end local validate_memory = function(x) + M.assert_greater_than('memory.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2) end local validate_network = function(x) + M.assert_greater_than('network.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2) end -local validate_pacman = function(x) +local validate_pacman = function(_) + -- nothing to check here end local validate_power = function(x) + M.assert_file_readable('power.battery', sys.SYSFS_POWER..'/'..x.battery) + for _, f in pairs(x.rapl_specs) do + M.assert_file_readable('power.rapl_specs', sys.SYSFS_RAPL..'/'..f.address) + end + M.assert_greater_than('power.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2) end local validate_processor = function(x) + -- NOTE need to check that the processor groups actually exist after querying + -- the cpu in the module + M.assert_greater_than('processor.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2) + for _, c in pairs(x.core_groups) do + M.assert_greater_than('processor.core_groups.[].rows', c.rows, 0) + end end local validate_readwrite = function(x) + M.assert_greater_than('readwrite.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2) end -local validate_system = function(x) +local validate_system = function(_) + -- nothing to check here end M.validate_config = function(config) @@ -195,7 +223,7 @@ M.validate_config = function(config) local t = block.type local d = block.data if t == "filesystem" then - validate_filesystem(d, column.width) + validate_filesystem(d) elseif t == "graphics" then validate_graphics(d) elseif t == "memory" then diff --git a/src/widget/text/text_internal.lua b/src/widget/text/text_internal.lua index 51e5db3..9486121 100644 --- a/src/widget/text/text_internal.lua +++ b/src/widget/text/text_internal.lua @@ -130,6 +130,7 @@ M.get_delta_y = function(y_align, font) local fe = set_font_extents(font) local descents = { top = fe.height, + -- TODO this 92 thing shouldn't be hardcoded (probably) center = 0.92 * fe.height * 0.5 - fe.descent, bottom = -fe.descent } From 96734cd328f9cb60b22285fbefeb15010d8387eb Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Sat, 30 Sep 2023 00:42:31 -0400 Subject: [PATCH 13/16] REF clean up some childish stuff --- src/validate.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/validate.lua b/src/validate.lua index e5547e8..c868700 100644 --- a/src/validate.lua +++ b/src/validate.lua @@ -127,6 +127,10 @@ M.assert_valid_pattern = function(what, pat) end end +M.validate_plot_ticks_y = function(what, x) + M.assert_greater_than(what..'.plot.ticks_y', x, 2) +end + -- TODO make spacing parameters aware of thickness, right now they start at the -- centry of lines, circles, etc. @@ -138,15 +142,15 @@ end local validate_graphics = function(x) M.assert_file_readable('graphics.dev_power', x.dev_power) - M.assert_greater_than('graphics.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2) + M.validate_plot_ticks_y('graphics.geometry', x.geometry.plot.ticks_y) end local validate_memory = function(x) - M.assert_greater_than('memory.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2) + M.validate_plot_ticks_y('memory.geometry', x.geometry.plot.ticks_y) end local validate_network = function(x) - M.assert_greater_than('network.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2) + M.validate_plot_ticks_y('network.geometry', x.geometry.plot.ticks_y) end local validate_pacman = function(_) @@ -171,7 +175,7 @@ local validate_processor = function(x) end local validate_readwrite = function(x) - M.assert_greater_than('readwrite.genometry.plot.ticks_y', x.geometry.plot.ticks_y, 2) + M.validate_plot_ticks_y('readwrite.geometry', x.geometry.plot.ticks_y) end local validate_system = function(_) From 882ac46259e805e3716c364b32e8943f870be932 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Sat, 30 Sep 2023 01:01:58 -0400 Subject: [PATCH 14/16] FIX weird blackout when certin batteries start discharging --- src/modules/power.lua | 11 ++++++++--- src/sys.lua | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/modules/power.lua b/src/modules/power.lua index c211c2e..a99fe3f 100644 --- a/src/modules/power.lua +++ b/src/modules/power.lua @@ -26,6 +26,8 @@ return function(update_freq, config, common, width, point) local mk_rate_plot = function(label, address, y) local read_joules = sys.intel_powercap_reader(address) + local read_joules0 = function() return read_joules() or 0 end + local read_joulesNA = function() return read_joules() or 'N/A' end local obj = common.make_rate_timeseries( point.x, y, @@ -38,12 +40,12 @@ return function(update_freq, config, common, width, point) label, 0, update_freq, - read_joules() + read_joules0() ) return common.mk_acc( width, plot_height + plot_sec_break, - function(_) common.update_rate_timeseries(obj, read_joules()) end, + function(_) common.update_rate_timeseries(obj, read_joulesNA()) end, mk_static(obj), mk_dynamic(obj) ) @@ -65,8 +67,11 @@ return function(update_freq, config, common, width, point) local mk_bat = function(y) local _read_battery_power = sys.battery_power_reader(config.battery) + -- TODO this is actually telling the plot to say it is on AC when the + -- battery status can't be read (in which case it should say ERROR or + -- something) local read_battery_power = function(is_using_ac) - return is_using_ac and 0 or _read_battery_power() + return is_using_ac and 0 or (_read_battery_power() or 0) end local read_bat_status = sys.battery_status_reader(config.battery) local obj = common.make_tagged_scaled_timeseries( diff --git a/src/sys.lua b/src/sys.lua index d83b0a3..8788af6 100644 --- a/src/sys.lua +++ b/src/sys.lua @@ -13,7 +13,12 @@ local dirname = function(s) end local read_micro = function(path) - return i_o.read_file(path, nil, '*n') * 0.000001 + local j = i_o.read_file(path, nil, '*n') + if j == nil then + return nil + else + return j * 0.000001 + end end local gmatch_to_table1 = function(pat, s) @@ -110,7 +115,15 @@ end M.battery_power_reader = function(battery) local current = format_power_path(battery, 'current_now') local voltage = format_power_path(battery, 'voltage_now') - return function() return read_micro(current) * read_micro(voltage) end + return function() + local c = read_micro(current) + local v = read_micro(voltage) + if c == nil or v == nil then + return nil + else + return c * v + end + end end M.battery_status_reader = function(battery) From a5183f4109b72725cda4e0f42c181984c70e70b7 Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Sat, 7 Oct 2023 16:15:11 -0400 Subject: [PATCH 15/16] FIX don't strickly check for graphics bus --- src/validate.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/validate.lua b/src/validate.lua index c868700..04f41a7 100644 --- a/src/validate.lua +++ b/src/validate.lua @@ -141,7 +141,8 @@ local validate_filesystem = function(x) end local validate_graphics = function(x) - M.assert_file_readable('graphics.dev_power', x.dev_power) + -- NOTE don't check that the bus exists because some optimus laptops 'turn + -- off' by pulling the entire kernel module and making the bus go poof M.validate_plot_ticks_y('graphics.geometry', x.geometry.plot.ticks_y) end From 738d365231d081076abaee6a31fae989edf50d8d Mon Sep 17 00:00:00 2001 From: ndwarshuis Date: Sun, 7 Jan 2024 11:06:51 -0500 Subject: [PATCH 16/16] ADD kinda useless battery monitor script in case I want it later --- scripts/batmon | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100755 scripts/batmon diff --git a/scripts/batmon b/scripts/batmon new file mode 100755 index 0000000..8544cde --- /dev/null +++ b/scripts/batmon @@ -0,0 +1,22 @@ +#! /bin/bash + +# small script to log battery usage in txt file for conky to parse +# lovingly stolen from battery-stats by petter reinholdtsen + +DST_FILE="$XDG_CACHE_HOME/batmon.log" +SYS_BAT_PATH="/sys/class/power_supply/BAT0" +BAT_CAP_PATH="$SYS_BAT_PATH/charge_now" +BAT_CAP_FULL_PATH="$SYS_BAT_PATH/charge_full" +MAX_ENTRIES=1440 # number of minutes in day + +read_bat_percent() { + timestamp=$(date +%s) + charge_now=$(cat "$BAT_CAP_PATH") + charge_full=$(cat "$BAT_CAP_FULL_PATH") + percent=$(echo "100 * $charge_now / $charge_full" | bc) + echo $timestamp $percent >> "$DST_FILE" +} + +read_bat_percent +# truncate to most recent entries by max_entries +tail -n $MAX_ENTRIES "$DST_FILE" > /tmp/batmon.tmp.log && mv /tmp/batmon.tmp.log "$DST_FILE"