more sophisticated syncronization in tidepool, enables graceful C-c

This commit is contained in:
tali 2023-04-13 20:14:09 -04:00
parent d1af51e204
commit cd73e8c592
3 changed files with 215 additions and 66 deletions

89
Cargo.lock generated
View File

@ -87,6 +87,16 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "ctrlc"
version = "3.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639"
dependencies = [
"nix",
"windows-sys 0.45.0",
]
[[package]]
name = "errno"
version = "0.2.8"
@ -177,7 +187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -189,7 +199,7 @@ dependencies = [
"hermit-abi",
"io-lifetimes",
"rustix",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -256,6 +266,18 @@ dependencies = [
"serde_json",
]
[[package]]
name = "nix"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
dependencies = [
"bitflags",
"cfg-if",
"libc",
"static_assertions",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -412,7 +434,7 @@ dependencies = [
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys",
"windows-sys 0.42.0",
]
[[package]]
@ -476,6 +498,12 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.10.0"
@ -518,6 +546,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"ctrlc",
"fish",
"mino",
"rand",
@ -696,46 +725,70 @@ dependencies = [
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "winnow"

View File

@ -20,3 +20,4 @@ toml = { version = "0.7" }
serde_json = { version = "1.0" }
clap = { version = "4.0", features = ["derive"] }
anyhow = { version = "1.0" }
ctrlc = { version = "3.2" }

View File

@ -2,6 +2,8 @@ use anyhow::{Context as _, Result};
use rand::RngCore as _;
use std::io::{Read, Write};
use std::path::Path;
use std::sync::{atomic, mpsc, Arc};
use std::time::Duration;
use fish::bot;
use tidepool::{cli, config, output, sim};
@ -18,36 +20,81 @@ fn main() -> Result<()> {
}
}
#[derive(Debug)]
enum Msg {
Interrupt,
Heartbeat(u64),
Status(u64, Box<Status>),
Done(u64, Box<output::Output>),
}
#[derive(Debug)]
struct Status {
pieces: usize,
lines_left: usize,
time: Duration,
}
fn create_mailbox() -> (mpsc::SyncSender<Msg>, mpsc::Receiver<Msg>) {
let (tx, rx) = mpsc::sync_channel(128);
let tx2 = tx.clone();
let on_ctrlc = move || {
if tx2.send(Msg::Interrupt).is_err() {
tracing::warn!("nobody handled ctrl-c, aborting");
std::process::exit(128 + ctrlc::Signal::SIGINT as i32);
}
};
if let Err(err) = ctrlc::set_handler(on_ctrlc) {
tracing::warn!("could not set signal handler: {err}");
}
(tx, rx)
}
fn single(args: cli::SingleRun) -> Result<()> {
let config = parse_config_file(&args.config_file)?;
let output = simulation(&args.io, &args.data, args.seed, config);
let (tx, rx) = create_mailbox();
let exit_early = Arc::new(atomic::AtomicBool::new(false));
let mut writer: Box<dyn std::io::Write> = match &args.output_file {
Some(path) => {
if path.is_file() {
if prompt_yn(&args.io, "output file already exists, overwrite?") {
tracing::debug!("removing {path:?}");
std::fs::remove_file(path)?;
} else {
tracing::warn!("output file already exists, exiting.");
return Ok(());
let exit_early2 = exit_early.clone();
let config2 = config.clone();
std::thread::spawn(move || run_simulation(&args.data, config2, args.seed, 0, tx, exit_early2));
while let Ok(msg) = rx.recv() {
tracing::trace!(msg = debug(&msg));
match msg {
Msg::Interrupt => {
eprintln!("\ncaught interrupt.");
tracing::debug!("interrupted");
exit_early.store(true, atomic::Ordering::Relaxed);
}
Msg::Heartbeat(_id) => {
tracing::debug!("heartbeat");
}
Msg::Status(_id, status) => {
if !args.io.quiet {
let ps = status.pieces;
let ll = status.lines_left;
let pps = ps as f64 / status.time.as_secs_f64();
eprintln!("#{ps}, {ll} lines left, {pps:.2} pps");
}
}
Box::new(
std::fs::File::create(path)
.with_context(|| format!("error opening output file '{}'", path.display()))?,
)
}
None => Box::new(std::io::stdout()),
};
Msg::Done(_id, output) => {
std::mem::drop(rx);
let pretty = args.output_file.is_none() && !args.io.quiet;
if pretty {
serde_json::to_writer_pretty(&mut writer, &output).context("error writing output")?;
writeln!(&mut writer).context("error writing output")?;
} else {
serde_json::to_writer(&mut writer, &output).context("error writing output")?;
if output.cleared < config.game.goal {
if !prompt_yn(&args.io, "run did not finish, keep it?") {
break;
}
}
write_output(&args.io, args.output_file.as_deref(), &output)?;
break;
}
}
}
Ok(())
@ -67,11 +114,41 @@ fn parse_config_file(path: &Path) -> Result<config::Config> {
toml::from_str(&contents).context("error parsing config file")
}
fn prompt_yn(io: &cli::IoArgs, msg: &str) -> bool {
if io.noninteractive {
return false;
fn write_output(io_args: &cli::IoArgs, path: Option<&Path>, output: &output::Output) -> Result<()> {
let mut writer: Box<dyn std::io::Write> = match &path {
Some(path) => {
if path.is_file() {
if prompt_yn(io_args, "output file already exists, overwrite?") {
tracing::debug!("removing {path:?}");
std::fs::remove_file(path)?;
} else {
tracing::warn!("output file already exists, exiting.");
return Ok(());
}
}
Box::new(
std::fs::File::create(path)
.with_context(|| format!("error opening output file '{}'", path.display()))?,
)
}
None => Box::new(std::io::stdout()),
};
let pretty = path.is_none() && !io_args.quiet;
if pretty {
serde_json::to_writer_pretty(&mut writer, &output)
.context("error writing output")
.and_then(|_| writeln!(&mut writer).context("error writing output"))
} else {
serde_json::to_writer(&mut writer, &output).context("error writing output")
}
let mut output = std::io::stdout().lock();
}
fn prompt_yn(io_args: &cli::IoArgs, msg: &str) -> bool {
if io_args.noninteractive {
return true;
}
let mut output = std::io::stderr().lock();
write!(output, "{msg} [y/N] ").unwrap();
output.flush().unwrap();
let mut user_input = String::new();
@ -79,15 +156,15 @@ fn prompt_yn(io: &cli::IoArgs, msg: &str) -> bool {
user_input.trim().eq_ignore_ascii_case("y")
}
fn simulation(
io: &cli::IoArgs,
data: &cli::OutputDataArgs,
seed: Option<u64>,
fn run_simulation(
data_args: &cli::OutputDataArgs,
config: config::Config,
) -> output::Output {
// TODO: status signals
let mut moves = data.list_moves.then(Vec::new);
seed: Option<u64>,
id: u64,
tx: mpsc::SyncSender<Msg>,
exit_early: Arc<atomic::AtomicBool>,
) {
let mut moves = data_args.list_moves.then(Vec::new);
let seed = seed.unwrap_or_else(|| rand::thread_rng().next_u64());
let sim_opts = sim::Options {
@ -100,9 +177,8 @@ fn simulation(
let time_start = std::time::Instant::now();
while sim.lines_left() > 0 {
// TODO: progress bar
if !io.quiet {
eprintln!("#{}, {} left", sim.pieces(), sim.lines_left());
if exit_early.load(atomic::Ordering::Relaxed) {
break;
}
let mut bot = bot::Bot::new(
@ -112,34 +188,53 @@ fn simulation(
for i in 0..config.bot.iters {
bot.think();
if i % 1000 == 999 {
tracing::debug!("iteration {i}");
// periodically check back with the main thread to see if we should exit
if i % 4096 == 4095 {
if exit_early.load(atomic::Ordering::Relaxed) {
break;
}
if tx.send(Msg::Heartbeat(id)).is_err() {
break;
}
}
}
if let Some(placement) = bot.suggest() {
sim.play(placement);
let Some(placement) = bot.suggest() else {
tracing::warn!("gave up :( pieces={}, id={id}", sim.pieces());
break;
};
if let Some(moves) = moves.as_mut() {
moves.push(output::Move { placement })
}
} else {
sim.play(placement);
if let Some(moves) = moves.as_mut() {
moves.push(output::Move { placement })
}
let status = Status {
lines_left: sim.lines_left(),
pieces: sim.pieces(),
time: std::time::Instant::now() - time_start,
};
if tx.send(Msg::Status(id, status.into())).is_err() {
break;
}
}
let time_end = std::time::Instant::now();
let profile = data.profile.then(|| output::Profile {
let profile = data_args.profile.then(|| output::Profile {
time: time_end - time_start,
});
output::Output {
let output = output::Output {
seed,
cleared: config.game.goal - sim.lines_left(),
pieces: sim.pieces(),
config: config.game,
moves,
profile,
};
if tx.send(Msg::Done(id, output.into())).is_err() {
tracing::debug!("nobody received output id={id}");
}
}