mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
feat: 超时崩溃记录带 last-pc 现场(pc/opcode/帧深)
This commit is contained in:
@@ -102,6 +102,7 @@ impl RunConfig {
|
|||||||
\x20 OXIDE_TEST262_SUPERVISORS concurrent supervised windows (default 16)\n\
|
\x20 OXIDE_TEST262_SUPERVISORS concurrent supervised windows (default 16)\n\
|
||||||
\x20 OXIDE_TEST262_RUNNING_LOG log every test as it starts (any value)\n\
|
\x20 OXIDE_TEST262_RUNNING_LOG log every test as it starts (any value)\n\
|
||||||
\x20 OXIDE_TEST262_HEARTBEAT heartbeat file path (supervised/chunked)\n\
|
\x20 OXIDE_TEST262_HEARTBEAT heartbeat file path (supervised/chunked)\n\
|
||||||
|
\x20 OXIDE_TEST262_PC_WATCH last-pc scene file path (supervised child appends pc/opcode/frames every 65536 instructions)\n\
|
||||||
\x20 OXIDE_TEST262_CHUNK_SIZE tests per child in chunked mode\n\
|
\x20 OXIDE_TEST262_CHUNK_SIZE tests per child in chunked mode\n\
|
||||||
\x20 OXIDE_TEST262_CHILD_CHUNK marks a spawned child as chunk worker (any value)\n\
|
\x20 OXIDE_TEST262_CHILD_CHUNK marks a spawned child as chunk worker (any value)\n\
|
||||||
\x20 OXIDE_TEST262_ALLOW_FAIL_EXIT child may exit 1 on fail (any value)\n\
|
\x20 OXIDE_TEST262_ALLOW_FAIL_EXIT child may exit 1 on fail (any value)\n\
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use oxide_kernel::kernel::{KernelConfig, KernelCore};
|
|||||||
use oxide_types::value::JsValue;
|
use oxide_types::value::JsValue;
|
||||||
use oxide_vm::vm::Vm;
|
use oxide_vm::vm::Vm;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, OnceLock, RwLock};
|
||||||
|
|
||||||
// 记录当前正在执行的测试路径(thread-local);每个测试执行前写入,
|
// 记录当前正在执行的测试路径(thread-local);每个测试执行前写入,
|
||||||
// panic hook 据此定位崩溃所在的测试文件。
|
// panic hook 据此定位崩溃所在的测试文件。
|
||||||
@@ -23,6 +23,17 @@ std::thread_local! {
|
|||||||
pub(crate) static CURRENT_TEST_PATH: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
|
pub(crate) static CURRENT_TEST_PATH: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 监督者经环境变量注入的 last-pc 现场文件路径(子进程单实例,每进程读一次):
|
||||||
|
/// 子进程运行期向该文件定频写现场行,监督者超时/崩溃杀子进程后读回末行。
|
||||||
|
static PC_WATCH_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// 读取监督者注入的 last-pc 现场文件路径(未注入返回 None)。
|
||||||
|
fn pc_watch_path() -> Option<&'static PathBuf> {
|
||||||
|
PC_WATCH_PATH
|
||||||
|
.get_or_init(|| std::env::var_os("OXIDE_TEST262_PC_WATCH").map(PathBuf::from))
|
||||||
|
.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
/// 在 catch_unwind 保护下运行单个测试,把引擎 panic 记为失败。
|
/// 在 catch_unwind 保护下运行单个测试,把引擎 panic 记为失败。
|
||||||
#[expect(clippy::too_many_arguments)]
|
#[expect(clippy::too_many_arguments)]
|
||||||
fn run_test(
|
fn run_test(
|
||||||
@@ -160,6 +171,10 @@ fn run_test_inner(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut vm = Vm::with_kernel_core(Arc::clone(kernel));
|
let mut vm = Vm::with_kernel_core(Arc::clone(kernel));
|
||||||
|
// 监督者注入现场文件路径时开启 VM 的 last-pc 定频写(未注入时零开销)。
|
||||||
|
if let Some(watch) = pc_watch_path() {
|
||||||
|
vm.set_pc_watch(Some(watch.clone()));
|
||||||
|
}
|
||||||
let run_result = vm.run(&Arc::new(module));
|
let run_result = vm.run(&Arc::new(module));
|
||||||
let dur = start.elapsed().as_millis() as u64;
|
let dur = start.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ pub(crate) struct RunStats {
|
|||||||
pub(crate) spawn_errors: usize,
|
pub(crate) spawn_errors: usize,
|
||||||
pub(crate) wait_errors: usize,
|
pub(crate) wait_errors: usize,
|
||||||
pub(crate) hb_write_errors: usize,
|
pub(crate) hb_write_errors: usize,
|
||||||
/// 超时/崩溃清单:(index, elapsed_ms);elapsed_ms=0 表示非超时崩溃。
|
/// 超时/崩溃清单:(index, elapsed_ms, scene);elapsed_ms=0 表示非超时崩溃,
|
||||||
pub(crate) timeout_crashes: Vec<(usize, u64)>,
|
/// scene 为挂死点的 last-pc 现场行(pc/opcode/flat_id/frames),缺文件/
|
||||||
|
/// 解析失败为 None。
|
||||||
|
pub(crate) timeout_crashes: Vec<(usize, u64, Option<String>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunStats {
|
impl RunStats {
|
||||||
@@ -159,27 +161,30 @@ mod tests {
|
|||||||
assert_eq!(a.fail_record_bytes, a.fail_records.iter().map(|r| r.message.len()).sum::<usize>());
|
assert_eq!(a.fail_record_bytes, a.fail_records.iter().map(|r| r.message.len()).sum::<usize>());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// merge 合并异常计数器:spawn/wait/hb 求和、timeout_crashes 拼接。
|
/// merge 合并异常计数器:spawn/wait/hb 求和、timeout_crashes 拼接(含 scene)。
|
||||||
#[test]
|
#[test]
|
||||||
fn runstats_merge_sums_error_counters() {
|
fn runstats_merge_sums_error_counters() {
|
||||||
let mut a = RunStats {
|
let mut a = RunStats {
|
||||||
spawn_errors: 1,
|
spawn_errors: 1,
|
||||||
wait_errors: 2,
|
wait_errors: 2,
|
||||||
hb_write_errors: 3,
|
hb_write_errors: 3,
|
||||||
timeout_crashes: vec![(0, 100)],
|
timeout_crashes: vec![(0, 100, None)],
|
||||||
..RunStats::default()
|
..RunStats::default()
|
||||||
};
|
};
|
||||||
let b = RunStats {
|
let b = RunStats {
|
||||||
spawn_errors: 4,
|
spawn_errors: 4,
|
||||||
wait_errors: 5,
|
wait_errors: 5,
|
||||||
hb_write_errors: 6,
|
hb_write_errors: 6,
|
||||||
timeout_crashes: vec![(1, 200)],
|
timeout_crashes: vec![(1, 200, Some("pc=9 op=ADD flat_id=0 frames=1".to_string()))],
|
||||||
..RunStats::default()
|
..RunStats::default()
|
||||||
};
|
};
|
||||||
a.merge(b);
|
a.merge(b);
|
||||||
assert_eq!(a.spawn_errors, 5);
|
assert_eq!(a.spawn_errors, 5);
|
||||||
assert_eq!(a.wait_errors, 7);
|
assert_eq!(a.wait_errors, 7);
|
||||||
assert_eq!(a.hb_write_errors, 9);
|
assert_eq!(a.hb_write_errors, 9);
|
||||||
assert_eq!(a.timeout_crashes, vec![(0, 100), (1, 200)]);
|
assert_eq!(
|
||||||
|
a.timeout_crashes,
|
||||||
|
vec![(0, 100, None), (1, 200, Some("pc=9 op=ADD flat_id=0 frames=1".to_string()))]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,20 +35,33 @@ fn merge_fail_log(stats: &mut RunStats, hb_path: &Path, consumed: &mut usize) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 读子进程被杀前留下的 last-pc 现场行:取文件末行,须含 pc/op/frames 三字段
|
||||||
|
/// 才接受(缺文件 / 空文件 / 残行均返回 None)。
|
||||||
|
fn read_pc_scene(path: &Path) -> Option<String> {
|
||||||
|
let content = std::fs::read_to_string(path).ok()?;
|
||||||
|
let last = content.lines().last()?.trim();
|
||||||
|
if last.starts_with("pc=") && last.contains(" op=") && last.contains(" frames=") {
|
||||||
|
Some(last.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 记一笔超时/崩溃结果:默认计入 skip,`--no-skip` 下计入 fail(归入
|
/// 记一笔超时/崩溃结果:默认计入 skip,`--no-skip` 下计入 fail(归入
|
||||||
/// `timeout/crash` 类别,父进程无法进一步拆分根因)。
|
/// `timeout/crash` 类别,父进程无法进一步拆分根因)。
|
||||||
///
|
///
|
||||||
/// # 副作用
|
/// # 副作用
|
||||||
/// - 把 `(index, elapsed_ms)` 追加进 timeout_crashes 清单(elapsed_ms=0 表示
|
/// - 把 `(index, elapsed_ms, scene)` 追加进 timeout_crashes 清单(elapsed_ms=0
|
||||||
/// 非超时崩溃:spawn 失败 / 中途退出 / waiterr / 无心跳),供收尾逐路径报告。
|
/// 表示非超时崩溃:spawn 失败 / 中途退出 / waiterr / 无心跳;scene 为挂死点
|
||||||
fn record_timeout_or_crash(stats: &mut RunStats, no_skip: bool, index: usize, elapsed_ms: u64) {
|
/// 的 last-pc 现场行,缺文件/解析失败为 None),供收尾逐路径报告。
|
||||||
|
fn record_timeout_or_crash(stats: &mut RunStats, no_skip: bool, index: usize, elapsed_ms: u64, scene: Option<&str>) {
|
||||||
if no_skip {
|
if no_skip {
|
||||||
stats.fail += 1;
|
stats.fail += 1;
|
||||||
*stats.fail_categories.entry("timeout/crash".into()).or_insert(0) += 1;
|
*stats.fail_categories.entry("timeout/crash".into()).or_insert(0) += 1;
|
||||||
} else {
|
} else {
|
||||||
stats.skip += 1;
|
stats.skip += 1;
|
||||||
}
|
}
|
||||||
stats.timeout_crashes.push((index, elapsed_ms));
|
stats.timeout_crashes.push((index, elapsed_ms, scene.map(str::to_string)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在监督下运行一个窗口 `[wstart, wend)`,返回经过多次子进程重启
|
/// 在监督下运行一个窗口 `[wstart, wend)`,返回经过多次子进程重启
|
||||||
@@ -69,6 +82,8 @@ fn supervise_window(
|
|||||||
// 旁路失败行已消费字节偏移:跨重启只并入新增段(见 merge_fail_log)。
|
// 旁路失败行已消费字节偏移:跨重启只并入新增段(见 merge_fail_log)。
|
||||||
let mut fails_consumed: usize = 0;
|
let mut fails_consumed: usize = 0;
|
||||||
let hb_path = std::env::temp_dir().join(format!("oxide_t262_hb_{}_{}.txt", std::process::id(), window_id));
|
let hb_path = std::env::temp_dir().join(format!("oxide_t262_hb_{}_{}.txt", std::process::id(), window_id));
|
||||||
|
// last-pc 现场文件:子进程运行期定频追加写,监督者杀子进程后读回末行。
|
||||||
|
let pc_path = std::env::temp_dir().join(format!("oxide_t262_pc_{}_{}.txt", std::process::id(), window_id));
|
||||||
|
|
||||||
let describe = |idx: usize| -> String {
|
let describe = |idx: usize| -> String {
|
||||||
paths
|
paths
|
||||||
@@ -85,6 +100,7 @@ fn supervise_window(
|
|||||||
// 新行是真实新事件而非重复并入。
|
// 新行是真实新事件而非重复并入。
|
||||||
let _ = std::fs::remove_file(&hb_path);
|
let _ = std::fs::remove_file(&hb_path);
|
||||||
let _ = std::fs::remove_file(format!("{}.tmp", hb_path.display()));
|
let _ = std::fs::remove_file(format!("{}.tmp", hb_path.display()));
|
||||||
|
let _ = std::fs::remove_file(&pc_path);
|
||||||
let max_tests = wend - cur;
|
let max_tests = wend - cur;
|
||||||
|
|
||||||
let mut child = match Command::new(exe)
|
let mut child = match Command::new(exe)
|
||||||
@@ -93,6 +109,7 @@ fn supervise_window(
|
|||||||
.env("OXIDE_MAX_TESTS", max_tests.to_string())
|
.env("OXIDE_MAX_TESTS", max_tests.to_string())
|
||||||
.env("OXIDE_TEST262_WORKERS", "1")
|
.env("OXIDE_TEST262_WORKERS", "1")
|
||||||
.env("OXIDE_TEST262_HEARTBEAT", &hb_path)
|
.env("OXIDE_TEST262_HEARTBEAT", &hb_path)
|
||||||
|
.env("OXIDE_TEST262_PC_WATCH", &pc_path)
|
||||||
.env("OXIDE_TEST262_CHILD_CHUNK", "1")
|
.env("OXIDE_TEST262_CHILD_CHUNK", "1")
|
||||||
.env("OXIDE_TEST262_ALLOW_FAIL_EXIT", "1")
|
.env("OXIDE_TEST262_ALLOW_FAIL_EXIT", "1")
|
||||||
.env_remove("OXIDE_TEST262_CHUNK_SIZE")
|
.env_remove("OXIDE_TEST262_CHUNK_SIZE")
|
||||||
@@ -104,7 +121,7 @@ fn supervise_window(
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!(" window {window_id}: failed to spawn child at index {cur}: {err}");
|
eprintln!(" window {window_id}: failed to spawn child at index {cur}: {err}");
|
||||||
stats.spawn_errors += 1;
|
stats.spawn_errors += 1;
|
||||||
record_timeout_or_crash(&mut stats, no_skip, cur, 0);
|
record_timeout_or_crash(&mut stats, no_skip, cur, 0, None);
|
||||||
cur += 1;
|
cur += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -127,12 +144,13 @@ fn supervise_window(
|
|||||||
merge_heartbeat(&mut stats, &hb);
|
merge_heartbeat(&mut stats, &hb);
|
||||||
let culprit = hb.index + 1;
|
let culprit = hb.index + 1;
|
||||||
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
|
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
|
||||||
|
let scene = read_pc_scene(&pc_path);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" [warn] window {window_id}: child exited ({status}) mid-test #{}: {}",
|
" [warn] window {window_id}: child exited ({status}) mid-test #{}: {}",
|
||||||
culprit,
|
culprit,
|
||||||
describe(culprit)
|
describe(culprit)
|
||||||
);
|
);
|
||||||
record_timeout_or_crash(&mut stats, no_skip, culprit, 0);
|
record_timeout_or_crash(&mut stats, no_skip, culprit, 0, scene.as_deref());
|
||||||
cur = culprit;
|
cur = culprit;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -140,7 +158,8 @@ fn supervise_window(
|
|||||||
" [warn] window {window_id}: child exited ({status}) with no heartbeat at index {cur}; skipping one"
|
" [warn] window {window_id}: child exited ({status}) with no heartbeat at index {cur}; skipping one"
|
||||||
);
|
);
|
||||||
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
|
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
|
||||||
record_timeout_or_crash(&mut stats, no_skip, cur, 0);
|
let scene = read_pc_scene(&pc_path);
|
||||||
|
record_timeout_or_crash(&mut stats, no_skip, cur, 0, scene.as_deref());
|
||||||
cur += 1;
|
cur += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +172,8 @@ fn supervise_window(
|
|||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
|
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
|
||||||
record_timeout_or_crash(&mut stats, no_skip, cur, 0);
|
let scene = read_pc_scene(&pc_path);
|
||||||
|
record_timeout_or_crash(&mut stats, no_skip, cur, 0, scene.as_deref());
|
||||||
cur += 1;
|
cur += 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -173,7 +193,7 @@ fn supervise_window(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if elapsed > deadline {
|
if elapsed > deadline {
|
||||||
// 先杀再读:心跳与旁路失败行都须在子进程死亡后取最终状态。
|
// 先杀再读:心跳、旁路失败行与 last-pc 现场都须在子进程死亡后取最终状态。
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
let hb = read_heartbeat(&hb_path);
|
let hb = read_heartbeat(&hb_path);
|
||||||
@@ -182,12 +202,14 @@ fn supervise_window(
|
|||||||
merge_heartbeat(&mut stats, h);
|
merge_heartbeat(&mut stats, h);
|
||||||
}
|
}
|
||||||
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
|
merge_fail_log(&mut stats, &hb_path, &mut fails_consumed);
|
||||||
|
let scene = read_pc_scene(&pc_path);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" [timeout] window {window_id}: TIMEOUT ({}s) on test #{culprit}: {}",
|
" [timeout] window {window_id}: TIMEOUT ({}s) on test #{culprit}: {}{}",
|
||||||
deadline.as_secs(),
|
deadline.as_secs(),
|
||||||
describe(culprit)
|
describe(culprit),
|
||||||
|
scene.as_deref().map(|s| format!(" [{s}]")).unwrap_or_default()
|
||||||
);
|
);
|
||||||
record_timeout_or_crash(&mut stats, no_skip, culprit, elapsed.as_millis() as u64);
|
record_timeout_or_crash(&mut stats, no_skip, culprit, elapsed.as_millis() as u64, scene.as_deref());
|
||||||
cur = culprit + 1;
|
cur = culprit + 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -198,6 +220,7 @@ fn supervise_window(
|
|||||||
|
|
||||||
let _ = std::fs::remove_file(&hb_path);
|
let _ = std::fs::remove_file(&hb_path);
|
||||||
let _ = std::fs::remove_file(format!("{}.tmp", hb_path.display()));
|
let _ = std::fs::remove_file(format!("{}.tmp", hb_path.display()));
|
||||||
|
let _ = std::fs::remove_file(&pc_path);
|
||||||
// 旁路失败行归档而非删除:统计已由心跳合并入父进程,此文件是收尾逐文件
|
// 旁路失败行归档而非删除:统计已由心跳合并入父进程,此文件是收尾逐文件
|
||||||
// 差分的终态依据(删除即永久丢记录)。
|
// 差分的终态依据(删除即永久丢记录)。
|
||||||
let _ = std::fs::rename(hb_path.with_extension("fails"), hb_path.with_extension("fails.done"));
|
let _ = std::fs::rename(hb_path.with_extension("fails"), hb_path.with_extension("fails.done"));
|
||||||
@@ -342,12 +365,15 @@ pub(crate) fn run_supervised(
|
|||||||
}
|
}
|
||||||
if !stats.timeout_crashes.is_empty() {
|
if !stats.timeout_crashes.is_empty() {
|
||||||
println!(" --- TIMEOUT/CRASH list ({}) ---", stats.timeout_crashes.len());
|
println!(" --- TIMEOUT/CRASH list ({}) ---", stats.timeout_crashes.len());
|
||||||
for (idx, elapsed_ms) in &stats.timeout_crashes {
|
for (idx, elapsed_ms, scene) in &stats.timeout_crashes {
|
||||||
let path = paths
|
let path = paths
|
||||||
.get(*idx)
|
.get(*idx)
|
||||||
.map(|p| p.display().to_string())
|
.map(|p| p.display().to_string())
|
||||||
.unwrap_or_else(|| format!("#{idx}"));
|
.unwrap_or_else(|| format!("#{idx}"));
|
||||||
println!(" {idx} {elapsed_ms}ms {path}");
|
match scene {
|
||||||
|
Some(s) => println!(" {idx} {elapsed_ms}ms {path} [{s}]"),
|
||||||
|
None => println!(" {idx} {elapsed_ms}ms {path}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("═══════════════════════════════════════");
|
println!("═══════════════════════════════════════");
|
||||||
@@ -360,17 +386,38 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::report::append_fail_log;
|
use crate::report::append_fail_log;
|
||||||
|
|
||||||
/// 超时/崩溃记账:计数(no_skip 转 fail)与 timeout_crashes 入列。
|
/// 超时/崩溃记账:计数(no_skip 转 fail)与 timeout_crashes 入列(scene 原样
|
||||||
|
/// 存串,缺现场为 None)。
|
||||||
#[test]
|
#[test]
|
||||||
fn record_timeout_or_crash_records_index_elapsed() {
|
fn record_timeout_or_crash_records_index_elapsed() {
|
||||||
let mut stats = RunStats::default();
|
let mut stats = RunStats::default();
|
||||||
record_timeout_or_crash(&mut stats, true, 3, 2500);
|
record_timeout_or_crash(&mut stats, true, 3, 2500, Some("pc=1 op=ADD flat_id=0 frames=1"));
|
||||||
assert_eq!(stats.fail, 1);
|
assert_eq!(stats.fail, 1);
|
||||||
assert_eq!(stats.fail_categories.get("timeout/crash"), Some(&1));
|
assert_eq!(stats.fail_categories.get("timeout/crash"), Some(&1));
|
||||||
assert_eq!(stats.timeout_crashes, vec![(3, 2500)]);
|
assert_eq!(stats.timeout_crashes, vec![(3, 2500, Some("pc=1 op=ADD flat_id=0 frames=1".to_string()))]);
|
||||||
record_timeout_or_crash(&mut stats, false, 4, 0);
|
record_timeout_or_crash(&mut stats, false, 4, 0, None);
|
||||||
assert_eq!(stats.skip, 1);
|
assert_eq!(stats.skip, 1);
|
||||||
assert_eq!(stats.timeout_crashes, vec![(3, 2500), (4, 0)]);
|
assert_eq!(
|
||||||
|
stats.timeout_crashes,
|
||||||
|
vec![(3, 2500, Some("pc=1 op=ADD flat_id=0 frames=1".to_string())), (4, 0, None)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// read_pc_scene 防御解析:缺文件 / 空文件 / 残行(缺 frames 字段)均 None,
|
||||||
|
/// 末行完整时接受(忽略中间行)。
|
||||||
|
#[test]
|
||||||
|
fn read_pc_scene_defensive_parse() {
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let path = dir.join(format!("oxide_t262_pc_test_{}.txt", std::process::id()));
|
||||||
|
assert_eq!(read_pc_scene(&path), None, "缺文件返回 None");
|
||||||
|
std::fs::write(&path, "").expect("写失败");
|
||||||
|
assert_eq!(read_pc_scene(&path), None, "空文件返回 None");
|
||||||
|
std::fs::write(&path, "pc=1 op=ADD flat_id=0 frames=1\npc=2 op=ADD flat_id=0 frames=1\n").expect("写失败");
|
||||||
|
assert_eq!(read_pc_scene(&path), Some("pc=2 op=ADD flat_id=0 frames=1".to_string()));
|
||||||
|
// 残行:末行缺 frames 字段,返回 None。
|
||||||
|
std::fs::write(&path, "pc=1 op=ADD flat_id=0 frames=1\npc=2 op=AD").expect("写失败");
|
||||||
|
assert_eq!(read_pc_scene(&path), None, "残行返回 None");
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// merge_fail_log 重建 fail_records:类别经 id 表重映射、消息 cap;
|
/// merge_fail_log 重建 fail_records:类别经 id 表重映射、消息 cap;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//! dispatch 主循环:逐指令解码与 OpCode 臂分派、执行期 GC 安全点、
|
//! dispatch 主循环:逐指令解码与 OpCode 臂分派、执行期 GC 安全点、
|
||||||
//! 步数/分配上限采样与二元运算宏 `binary_arith!`。
|
//! 步数/分配上限采样与二元运算宏 `binary_arith!`。
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
use num_traits::Zero;
|
use num_traits::Zero;
|
||||||
use oxide_bytecode::opcode::{self, OpCode};
|
use oxide_bytecode::opcode::{self, OpCode};
|
||||||
use oxide_runtime_api as coercion;
|
use oxide_runtime_api as coercion;
|
||||||
@@ -216,6 +218,21 @@ impl Vm {
|
|||||||
eprintln!("pc={pc:4} {op:>16} rd={rd} a={a} b={b}");
|
eprintln!("pc={pc:4} {op:>16} rd={rd} a={a} b={b}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// last-pc 现场:开启时每 2^16 指令追加写一行 pc/opcode/flat_id/frames,
|
||||||
|
// 供监督者超时/崩溃杀子进程后读回挂死点(精度 = 距挂死点 2^16 指令内);
|
||||||
|
// 关闭时零输出零分配。
|
||||||
|
if let Some(watch) = self.pc_watch.as_ref() {
|
||||||
|
if (steps & 0xFFFF) == 0 {
|
||||||
|
let line =
|
||||||
|
format!("pc={pc} op={op} flat_id={} frames={}\n", self.active_flat_id, self.frames.len());
|
||||||
|
let _ = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(watch)
|
||||||
|
.and_then(|mut f| f.write_all(line.as_bytes()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match op {
|
match op {
|
||||||
OpCode::NOP => {}
|
OpCode::NOP => {}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#![allow(clippy::arc_with_non_send_sync)]
|
#![allow(clippy::arc_with_non_send_sync)]
|
||||||
|
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
@@ -354,6 +355,10 @@ pub struct Vm {
|
|||||||
/// `pc + opcode + 操作数`。运行时开关,旁路 tracing 级别上限(release
|
/// `pc + opcode + 操作数`。运行时开关,旁路 tracing 级别上限(release
|
||||||
/// 构建可用);默认关闭,关闭时热路径仅一次 bool 比较,零输出零分配。
|
/// 构建可用);默认关闭,关闭时热路径仅一次 bool 比较,零输出零分配。
|
||||||
pub trace_instructions: bool,
|
pub trace_instructions: bool,
|
||||||
|
/// last-pc 现场文件路径:开启时 dispatch 主循环每 2^16 指令追加写一行
|
||||||
|
/// `pc/opcode/flat_id/frames`,供监督者超时/崩溃杀子进程后读回挂死点;
|
||||||
|
/// 默认 None(零输出零分配)。
|
||||||
|
pub(crate) pc_watch: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Vm {
|
impl Drop for Vm {
|
||||||
@@ -423,6 +428,13 @@ impl Vm {
|
|||||||
self.trace_instructions = on;
|
self.trace_instructions = on;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 设置 last-pc 现场文件路径(开启后 dispatch 主循环每 2^16 指令追加写一行
|
||||||
|
/// `pc/opcode/flat_id/frames`,供监督者超时/崩溃杀子进程后读回挂死点);
|
||||||
|
/// None 清除。
|
||||||
|
pub fn set_pc_watch(&mut self, path: Option<PathBuf>) {
|
||||||
|
self.pc_watch = path;
|
||||||
|
}
|
||||||
|
|
||||||
/// 只读访问 VM 的 epoch arena。
|
/// 只读访问 VM 的 epoch arena。
|
||||||
pub fn epoch(&self) -> &Epoch {
|
pub fn epoch(&self) -> &Epoch {
|
||||||
&self.epoch
|
&self.epoch
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ impl Vm {
|
|||||||
saved_flat_id_stack: Vec::new(),
|
saved_flat_id_stack: Vec::new(),
|
||||||
saved_table_gen_stack: Vec::new(),
|
saved_table_gen_stack: Vec::new(),
|
||||||
trace_instructions: false,
|
trace_instructions: false,
|
||||||
|
pc_watch: None,
|
||||||
};
|
};
|
||||||
vm.init_generator_intrinsics();
|
vm.init_generator_intrinsics();
|
||||||
vm.init_promise_intrinsics();
|
vm.init_promise_intrinsics();
|
||||||
@@ -293,6 +294,7 @@ impl Vm {
|
|||||||
saved_flat_id_stack: Vec::new(),
|
saved_flat_id_stack: Vec::new(),
|
||||||
saved_table_gen_stack: Vec::new(),
|
saved_table_gen_stack: Vec::new(),
|
||||||
trace_instructions: false,
|
trace_instructions: false,
|
||||||
|
pc_watch: None,
|
||||||
};
|
};
|
||||||
vm.init_generator_intrinsics();
|
vm.init_generator_intrinsics();
|
||||||
vm.init_promise_intrinsics();
|
vm.init_promise_intrinsics();
|
||||||
|
|||||||
Reference in New Issue
Block a user