From 12f01318c29e23973e03ba1bf0ec9abf8a2a9a8b Mon Sep 17 00:00:00 2001 From: Delyan Angelov Date: Fri, 11 Nov 2022 22:06:42 +0200 Subject: [PATCH] v.live, cgen: enable using `[live]` in modules too (monitor used .v files for changes, not just the top level one) (#16396) --- vlib/v/gen/c/cgen.v | 2 ++ vlib/v/gen/c/fn.v | 1 + vlib/v/gen/c/live.v | 15 ++++++++++- vlib/v/live/common.v | 8 +++--- vlib/v/live/executable/reloader.v | 42 ++++++++++++++++++++++++++++--- vlib/v/live/live_test.v | 30 +++++++++++++--------- vlib/v/live/live_test_template.vv | 22 +++++----------- 7 files changed, 83 insertions(+), 37 deletions(-) diff --git a/vlib/v/gen/c/cgen.v b/vlib/v/gen/c/cgen.v index f5d61e83a0..69f78a39c5 100644 --- a/vlib/v/gen/c/cgen.v +++ b/vlib/v/gen/c/cgen.v @@ -168,6 +168,7 @@ mut: json_types []ast.Type // to avoid json gen duplicates pcs []ProfileCounterMeta // -prof profile counter fn_names => fn counter name hotcode_fn_names []string + hotcode_fpaths []string embedded_files []ast.EmbeddedFile sql_i int sql_stmt_name string @@ -404,6 +405,7 @@ pub fn gen(files []&ast.File, table &ast.Table, pref &pref.Preferences) (string, global_g.pcs << g.pcs global_g.json_types << g.json_types global_g.hotcode_fn_names << g.hotcode_fn_names + global_g.hotcode_fpaths << g.hotcode_fpaths global_g.test_function_names << g.test_function_names unsafe { g.free_builders() } for k, v in g.autofree_methods { diff --git a/vlib/v/gen/c/fn.v b/vlib/v/gen/c/fn.v index 5f8ea52928..038920e0fe 100644 --- a/vlib/v/gen/c/fn.v +++ b/vlib/v/gen/c/fn.v @@ -245,6 +245,7 @@ fn (mut g Gen) gen_fn_decl(node &ast.FnDecl, skip bool) { // with 'impl_live_' . if is_livemain { g.hotcode_fn_names << name + g.hotcode_fpaths << g.file.path } mut impl_fn_name := name if is_live_wrap { diff --git a/vlib/v/gen/c/live.v b/vlib/v/gen/c/live.v index 19bcd2bc1b..2bba24a107 100644 --- a/vlib/v/gen/c/live.v +++ b/vlib/v/gen/c/live.v @@ -1,5 +1,6 @@ module c +import os import v.pref import v.util @@ -95,9 +96,21 @@ fn (mut g Gen) generate_hotcode_reloading_main_caller() { g.writeln('\t\t\t\t\t &live_fn_mutex,') g.writeln('\t\t\t\t\t v_bind_live_symbols') g.writeln('\t\t);') + mut already_added := map[string]bool{} + for f in g.hotcode_fpaths { + already_added[f] = true + } + mut idx := 0 + for f, _ in already_added { + fpath := os.real_path(f) + g.writeln('\t\tv__live__executable__add_live_monitored_file(live_info, ${ctoslit(fpath)}); // source V file with [live] ${ + idx + 1}/$already_added.len') + idx++ + } + g.writeln('') // g_live_info gives access to the LiveReloadInfo methods, // to the custom user code, through calling v_live_info() - g.writeln('\t\t g_live_info = (void*)live_info;') + g.writeln('\t\tg_live_info = (void*)live_info;') g.writeln('\t\tv__live__executable__start_reloader(live_info);') g.writeln('\t}\t// end of live code initialization section') g.writeln('') diff --git a/vlib/v/live/common.v b/vlib/v/live/common.v index 12ec01a4a6..cffbd83199 100644 --- a/vlib/v/live/common.v +++ b/vlib/v/live/common.v @@ -1,14 +1,11 @@ module live -pub const ( - is_used = 1 -) +pub const is_used = 1 pub type FNLinkLiveSymbols = fn (linkcb voidptr) pub type FNLiveReloadCB = fn (info &LiveReloadInfo) -[minify] pub struct LiveReloadInfo { pub: vexe string // full path to the v compiler @@ -17,8 +14,9 @@ pub: live_fn_mutex voidptr // the address of the C mutex, that locks the [live] fns during reloads. live_linkfn FNLinkLiveSymbols // generated C callback; receives a dlopen handle so_extension string // .so or .dll - so_name_template string // a sprintf template for the shared libraries location + so_name_template string // a template for the shared libraries location pub mut: + monitored_files []string // an array, containing all paths that should be monitored for changes live_lib voidptr // the result of dl.open reloads int // how many times a reloading was tried reloads_ok int // how many times the reloads succeeded diff --git a/vlib/v/live/executable/reloader.v b/vlib/v/live/executable/reloader.v index a532e9c1e0..f8be274123 100644 --- a/vlib/v/live/executable/reloader.v +++ b/vlib/v/live/executable/reloader.v @@ -46,6 +46,19 @@ pub fn start_reloader(mut r live.LiveReloadInfo) { spawn reloader(mut r) } +// add_live_monitored_file will be called by the generated code inside main(), to add a list of all the .v files +// that were used during the main program compilation. Any change to any of them, will later trigger a +// recompilation and reloading of the produced shared library. This makes it possible for [live] functions +// inside modules to also work, not just in the top level program. +pub fn add_live_monitored_file(mut lri live.LiveReloadInfo, path string) { + mtime := os.file_last_mod_unix(path) + lri.monitored_files << path + elog(lri, '${@FN} mtime: ${mtime:12} path: $path') + if lri.last_mod_ts < mtime { + lri.last_mod_ts = mtime + } +} + [if debuglive ?] fn elog(r &live.LiveReloadInfo, s string) { eprintln(s) @@ -126,13 +139,21 @@ fn protected_load_lib(mut r live.LiveReloadInfo, new_lib_path string) { // Note: r.reloader() is executed in a new, independent thread fn reloader(mut r live.LiveReloadInfo) { // elog(r,'reloader, r: $r') - mut last_ts := os.file_last_mod_unix(r.original) + mut last_ts := r.last_mod_ts + mut monitored_file_paths := r.monitored_files.clone() + // it is much more likely that the user will be changing *the latest* files + // => put them first, so the search can be cut earlier: + monitored_file_paths.reverse_in_place() for { if r.cb_recheck != unsafe { nil } { r.cb_recheck(r) } - now_ts := os.file_last_mod_unix(r.original) - if last_ts != now_ts { + sw := time.new_stopwatch() + now_ts := get_latest_ts_from_monitored_files(monitored_file_paths, last_ts) + $if trace_check_monitored_files ? { + eprintln('check if last_ts: $last_ts < now_ts: $now_ts , took $sw.elapsed().microseconds() microseconds') + } + if last_ts < now_ts { r.reloads++ last_ts = now_ts r.last_mod_ts = last_ts @@ -157,3 +178,18 @@ fn reloader(mut r live.LiveReloadInfo) { } } } + +fn get_latest_ts_from_monitored_files(monitored_file_paths []string, last_ts i64) i64 { + mut latest_ts := i64(0) + for f in monitored_file_paths { + mtime := os.file_last_mod_unix(f) + if mtime > latest_ts { + latest_ts = mtime + if mtime > last_ts { + // no need to check further, since we already know, that there is a newer file, so return early its timestamp + return mtime + } + } + } + return latest_ts +} diff --git a/vlib/v/live/live_test.v b/vlib/v/live/live_test.v index a48c1ee4f1..c0a2f4489c 100644 --- a/vlib/v/live/live_test.v +++ b/vlib/v/live/live_test.v @@ -34,8 +34,9 @@ TODO: Cleanup this when/if v has better process control/communication primitives const ( vexe = os.getenv('VEXE') vtmp_folder = os.join_path(os.vtmp_dir(), 'v', 'tests', 'live') - tmp_file = os.join_path(vtmp_folder, 'generated_live_program.tmp.v') - source_file = os.join_path(vtmp_folder, 'generated_live_program.v') + main_source_file = os.join_path(vtmp_folder, 'main.v') + tmp_file = os.join_path(vtmp_folder, 'mymodule', 'generated_live_module.tmp') + source_file = os.join_path(vtmp_folder, 'mymodule', 'mymodule.v') genexe_file = os.join_path(vtmp_folder, 'generated_live_program') output_file = os.join_path(vtmp_folder, 'generated_live_program.output.txt') res_original_file = os.join_path(vtmp_folder, 'ORIGINAL.txt') @@ -50,14 +51,6 @@ fn get_source_template() string { return src.replace('#OUTPUT_FILE#', output_file) } -fn edefault(name string, default string) string { - res := os.getenv(name) - if res == '' { - return default - } - return res -} - fn atomic_write_source(source string) { // Note: here wrtiting is done in 2 steps, since os.write_file can take some time, // during which the file will be modified, but it will still be not completely written. @@ -70,6 +63,14 @@ fn atomic_write_source(source string) { fn testsuite_begin() { os.rmdir_all(vtmp_folder) or {} os.mkdir_all(vtmp_folder) or {} + os.mkdir_all(os.join_path(vtmp_folder, 'mymodule'))! + os.write_file(os.join_path(vtmp_folder, 'v.mod'), '')! + os.write_file(os.join_path(vtmp_folder, 'main.v'), ' +import mymodule +fn main() { + mymodule.mymain() +} +')! if os.user_os() !in ['linux', 'solaris'] && os.getenv('FORCE_LIVE_TEST').len == 0 { eprintln('Testing the runtime behaviour of -live mode,') eprintln('is reliable only on Linux/macOS for now.') @@ -77,6 +78,7 @@ fn testsuite_begin() { exit(0) } atomic_write_source(live_program_source) + // os.system('tree $vtmp_folder') exit(1) } [debuglivetest] @@ -85,6 +87,7 @@ fn vprintln(s string) { } fn testsuite_end() { + // os.system('tree $vtmp_folder') exit(1) vprintln('source: $source_file') vprintln('output: $output_file') vprintln('---------------------------------------------------------------------------') @@ -117,7 +120,8 @@ fn wait_for_file(new string) { time.sleep(100 * time.millisecond) expected_file := os.join_path(vtmp_folder, new + '.txt') eprintln('waiting for $expected_file ...') - max_wait_cycles := edefault('WAIT_CYCLES', '1').int() + // os.system('tree $vtmp_folder') + max_wait_cycles := os.getenv_opt('WAIT_CYCLES') or { '1' }.int() for i := 0; i <= max_wait_cycles; i++ { if i % 25 == 0 { vprintln(' checking ${i:-10d} for $expected_file ...') @@ -147,7 +151,9 @@ fn setup_cycles_environment() { fn test_live_program_can_be_compiled() { setup_cycles_environment() eprintln('Compiling...') - os.system('${os.quoted_path(vexe)} -nocolor -live -o ${os.quoted_path(genexe_file)} ${os.quoted_path(source_file)}') + compile_cmd := '${os.quoted_path(vexe)} -cg -keepc -nocolor -live -o ${os.quoted_path(genexe_file)} ${os.quoted_path(main_source_file)}' + eprintln('> compile_cmd: $compile_cmd') + os.system(compile_cmd) // cmd := '${os.quoted_path(genexe_file)} > /dev/null &' eprintln('Running with: $cmd') diff --git a/vlib/v/live/live_test_template.vv b/vlib/v/live/live_test_template.vv index c781ddb7b7..187a615104 100644 --- a/vlib/v/live/live_test_template.vv +++ b/vlib/v/live/live_test_template.vv @@ -1,4 +1,4 @@ -module main +module mymodule import time import os @@ -29,30 +29,20 @@ fn myprintln(s string) { [live] fn pmessage() string { - _ := some_constant * math.sin(2.0 * math.pi * f64(time.ticks() % 6000) / 6000) + _ := mymodule.some_constant * math.sin(2.0 * math.pi * f64(time.ticks() % 6000) / 6000) return 'ORIGINAL' } -const ( - delay = 20 -) +const delay = 20 -fn edefault(name string, default string) string { - res := os.getenv(name) - if res == '' { - return default - } - return res -} - -fn main() { +pub fn mymain() { mut info := live.info() info.recheck_period_ms = 5 myprintln('START') myprintln('DATE: ' + time.now().str()) pmessage() pmessage() - max_cycles := edefault('LIVE_CYCLES', '1').int() + max_cycles := os.getenv_opt('LIVE_CYCLES') or { '1' }.int() // NB: 1000 * 20 = maximum of ~20s runtime for i := 0; i < max_cycles; i++ { s := pmessage() @@ -61,7 +51,7 @@ fn main() { if s == 'STOP' { break } - time.sleep(delay * time.millisecond) + time.sleep(mymodule.delay * time.millisecond) } pmessage() pmessage()