Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bench-cargo-miri/mse/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ static EXPECTED: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
static PCM: &[i16] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, -2, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 2, 0, 2, 0, 2, 0, 2, 0, 2, 0, 1, 0, 1, 0, 0, -2, 0, -2, 0, -2, 0, -2, -2, -2, -3, -3, -3, -3, -4, -2, -5, -2, -5, -2, -4, 0, -4, 0, -4, 0, -4, 1, -4, 1, -4, 2, -4, 2, -4, 2, -4, 2, -4, 2, -3, 1, -4, 0, -4, 0, -5, 0, -5, 0, -5, 0, -4, 2, -4, 3, -4, 4, -3, 5, -2, 5, -3, 6, -3, 6, -3, 5, -3, 5, -2, 4, -2, 3, -5, 0, -6, 0, -3, -2, -4, -4, -9, -5, -9, -4, -4, -2, -4, -2, -4, 0, -2, 1, 1, 1, 4, 2, 8, 2, 12, 1, 13, 0, 12, 0, 11, 0, 8, -2, 7, 0, 7, -3, 11, -8, 15, -9, 17, -6, 17, -5, 13, -3, 7, 0, 3, 0, -2, 0, -4, 0, -4, -2, -6, 0, -14, -2, -17, -4, -8, 0, -7, 5, -17, 7, -18, 10, -7, 18, -2, 25, -3, 27, 0, 31, 4, 34, 4, 34, 8, 36, 8, 37, 2, 36, 4, 34, 8, 28, 3, 15, 0, 11, 0, 12, -5, 8, -4, 10, 0, 23, -4, 31, -8, 30, -2, 30, 0, 26, -6, 22, -6, 20, -12, 15, -19, 10, -10, 13, -14, 6, -43, -13, -43, -16, -9, -12, -10, -29, -42, -40, -37, -28, -5, -21, 1, -24, -8, -20, 4, -18, 26, -24, 44, -26, 66, -30, 86, -37, 88, -41, 72, -46, 50, -31, 28, 23, 14, 64, 16, 51, 26, 32, 34, 39, 42, 48, 35, 58, 0, 72, -36, 69, -59, 58, -98, 54, -124, 36, -103, 12, -110, 5, -173, -19, -146, -59, -4, -42, 51, 1, -23, -6, -30, -6, 45, 46, 47, 70, 6, 55, 19, 60, 38, 62, 42, 47, 61, 46, 40, 42, -19, 22, -34, 6, -35, -50, -61, -141, -37, -171, 17, -163, 26, -180, 46, -154, 80, -63, 48, -4, 18, 20, 50, 47, 58, 53, 44, 61, 57, 85, 37, 80, 0, 86, -8, 106, -95, 49, -213, -8, -131, 47, 49, 63, 40, -39, -69, -74, -37, -20, 63, -12, 58, -14, -12, 25, -31, 41, 11, 45, 76, 47, 167, 5, 261, -37, 277, -83, 183, -172, 35, -122, -79, 138, -70, 266, 69, 124, 228, 0, 391, -29, 594, -84, 702, -78, 627, -8, 551, -13, 509, 13, 372, 120, 352, 125, 622, 127, 691, 223, 362, 126, 386, -33, 915, 198, 958, 457, 456, 298, 500, 233, 1027, 469, 1096, 426, 918, 160, 1067, 141, 1220, 189, 1245, 164, 1375, 297, 1378, 503, 1299, 702, 1550, 929, 1799, 855, 1752, 547, 1830, 602, 1928, 832, 1736, 796, 1735, 933, 1961, 1385, 1935, 1562, 2105, 1485, 2716, 1449, 2948, 1305, 2768, 1205, 2716, 1346, 2531, 1450, 2470, 1653, 3117, 2111, 3370, 2176, 2696, 1947, 2925, 2305, 3846, 2658, 2425, 2184, -877, 1981, -2261, 2623, -1645, 2908, -1876, 2732, -2704, 2953, -2484, 3116, -2120, 2954, -2442, 3216, -2466, 3499, -2192, 3234, -2392, 3361, -2497, 3869, -2078, 3772, -1858, 3915, -2066, 4438, -2285, 2934, -2294, -280, -2066, -1762, -1992, -1412, -2298, -1535, -2399, -1789, -2223, -1419, -2244, -1334, -2092, -1476, -1777, -1396, -2014, -1571, -2199, -1574, -1843, -1167, -1910, -1446, -2007, -1818];

fn main() {
for i in 0..5 {
for _ in 0..2 {
mse(PCM.len(), PCM, EXPECTED);
}
}
Expand Down
2 changes: 1 addition & 1 deletion rust-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
d9051341a1c142542a3f7dab509266606c775382
e86c9e6ef8be7ddec0360f20aae7d86c69c59a83
15 changes: 8 additions & 7 deletions src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub fn create_ecx<'mir, 'tcx: 'mir>(
),
);
// Complete initialization.
MemoryExtra::init_extern_statics(&mut ecx)?;
EnvVars::init(&mut ecx, config.excluded_env_vars);

// Setup first stack-frame
Expand Down Expand Up @@ -90,14 +91,14 @@ pub fn create_ecx<'mir, 'tcx: 'mir>(
// Make space for `0` terminator.
let size = arg.len() as u64 + 1;
let arg_type = tcx.mk_array(tcx.types.u8, size);
let arg_place = ecx.allocate(ecx.layout_of(arg_type)?, MiriMemoryKind::Env.into());
let arg_place = ecx.allocate(ecx.layout_of(arg_type)?, MiriMemoryKind::Machine.into());
ecx.write_os_str_to_c_str(OsStr::new(arg), arg_place.ptr, size)?;
argvs.push(arg_place.ptr);
}
// Make an array with all these pointers, in the Miri memory.
let argvs_layout =
ecx.layout_of(tcx.mk_array(tcx.mk_imm_ptr(tcx.types.u8), argvs.len() as u64))?;
let argvs_place = ecx.allocate(argvs_layout, MiriMemoryKind::Env.into());
let argvs_place = ecx.allocate(argvs_layout, MiriMemoryKind::Machine.into());
for (idx, arg) in argvs.into_iter().enumerate() {
let place = ecx.mplace_field(argvs_place, idx as u64)?;
ecx.write_scalar(arg, place.into())?;
Expand All @@ -108,13 +109,13 @@ pub fn create_ecx<'mir, 'tcx: 'mir>(
// Store `argc` and `argv` for macOS `_NSGetArg{c,v}`.
{
let argc_place =
ecx.allocate(ecx.layout_of(tcx.types.isize)?, MiriMemoryKind::Env.into());
ecx.allocate(ecx.layout_of(tcx.types.isize)?, MiriMemoryKind::Machine.into());
ecx.write_scalar(argc, argc_place.into())?;
ecx.machine.argc = Some(argc_place.ptr);

let argv_place = ecx.allocate(
ecx.layout_of(tcx.mk_imm_ptr(tcx.types.unit))?,
MiriMemoryKind::Env.into(),
MiriMemoryKind::Machine.into(),
);
ecx.write_scalar(argv, argv_place.into())?;
ecx.machine.argv = Some(argv_place.ptr);
Expand All @@ -134,7 +135,7 @@ pub fn create_ecx<'mir, 'tcx: 'mir>(

let cmd_utf16: Vec<u16> = cmd.encode_utf16().collect();
let cmd_type = tcx.mk_array(tcx.types.u16, cmd_utf16.len() as u64);
let cmd_place = ecx.allocate(ecx.layout_of(cmd_type)?, MiriMemoryKind::Env.into());
let cmd_place = ecx.allocate(ecx.layout_of(cmd_type)?, MiriMemoryKind::Machine.into());
ecx.machine.cmd_line = Some(cmd_place.ptr);
// Store the UTF-16 string. We just allocated so we know the bounds are fine.
let char_size = Size::from_bytes(2);
Expand All @@ -147,7 +148,7 @@ pub fn create_ecx<'mir, 'tcx: 'mir>(
};

// Return place (in static memory so that it does not count as leak).
let ret_place = ecx.allocate(ecx.layout_of(tcx.types.isize)?, MiriMemoryKind::Env.into());
let ret_place = ecx.allocate(ecx.layout_of(tcx.types.isize)?, MiriMemoryKind::Machine.into());
// Call start function.
ecx.call_function(
start_instance,
Expand All @@ -158,7 +159,7 @@ pub fn create_ecx<'mir, 'tcx: 'mir>(

// Set the last_error to 0
let errno_layout = ecx.layout_of(tcx.types.u32)?;
let errno_place = ecx.allocate(errno_layout, MiriMemoryKind::Env.into());
let errno_place = ecx.allocate(errno_layout, MiriMemoryKind::Machine.into());
ecx.write_scalar(Scalar::from_u32(0), errno_place.into())?;
ecx.machine.last_error = Some(errno_place);

Expand Down
11 changes: 6 additions & 5 deletions src/intptrcast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::collections::{hash_map::Entry, HashMap};
use rand::Rng;

use rustc::ty::layout::HasDataLayout;
use rustc_mir::interpret::{AllocCheck, AllocId, InterpResult, Memory, Pointer, PointerArithmetic};
use rustc_mir::interpret::{AllocCheck, AllocId, InterpResult, Memory, Machine, Pointer, PointerArithmetic};
use rustc_target::abi::Size;

use crate::{Evaluator, Tag, STACK_ADDR};
Expand Down Expand Up @@ -80,12 +80,13 @@ impl<'mir, 'tcx> GlobalState {
) -> InterpResult<'tcx, u64> {
let mut global_state = memory.extra.intptrcast.borrow_mut();
let global_state = &mut *global_state;
let id = Evaluator::canonical_alloc_id(memory, ptr.alloc_id);

// There is nothing wrong with a raw pointer being cast to an integer only after
// it became dangling. Hence `MaybeDead`.
let (size, align) = memory.get_size_and_align(ptr.alloc_id, AllocCheck::MaybeDead)?;
let (size, align) = memory.get_size_and_align(id, AllocCheck::MaybeDead)?;

let base_addr = match global_state.base_addr.entry(ptr.alloc_id) {
let base_addr = match global_state.base_addr.entry(id) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
// This allocation does not have a base address yet, pick one.
Expand All @@ -102,7 +103,7 @@ impl<'mir, 'tcx> GlobalState {
trace!(
"Assigning base address {:#x} to allocation {:?} (slack: {}, align: {})",
base_addr,
ptr.alloc_id,
id,
slack,
align.bytes(),
);
Expand All @@ -112,7 +113,7 @@ impl<'mir, 'tcx> GlobalState {
global_state.next_base_addr = base_addr.checked_add(max(size.bytes(), 1)).unwrap();
// Given that `next_base_addr` increases in each allocation, pushing the
// corresponding tuple keeps `int_to_ptr_map` sorted
global_state.int_to_ptr_map.push((base_addr, ptr.alloc_id));
global_state.int_to_ptr_map.push((base_addr, id));

base_addr
}
Expand Down
82 changes: 55 additions & 27 deletions src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::num::NonZeroU64;
use std::rc::Rc;

Expand All @@ -12,10 +13,9 @@ use rustc::mir;
use rustc::ty::{
self,
layout::{LayoutOf, Size},
Ty, TyCtxt,
Ty,
};
use rustc_ast::attr;
use rustc_hir::def_id::DefId;
use rustc_span::{source_map::Span, symbol::sym};

use crate::*;
Expand Down Expand Up @@ -48,8 +48,8 @@ pub enum MiriMemoryKind {
C,
/// Windows `HeapAlloc` memory.
WinHeap,
/// Memory for env vars and args, errno and other parts of the machine-managed environment.
Env,
/// Memory for env vars and args, errno, extern statics and other parts of the machine-managed environment.
Machine,
/// Rust statics.
Static,
}
Expand All @@ -74,7 +74,11 @@ pub struct MemoryExtra {
pub stacked_borrows: Option<stacked_borrows::MemoryExtra>,
pub intptrcast: intptrcast::MemoryExtra,

/// Mapping extern static names to their canonical allocation.
pub(crate) extern_statics: HashMap<&'static str, AllocId>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be keyed on Symbol instead.
Also, does miri not use FxHashMaps? They're an easy speed boost for rustc but idk how relevant that is here.

(Sorry if this stuff has been discussed before in the context of miri, I haven't personally seen anything about this)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Miri doesn't currently use FxHashMap but probably should, yeah.

As for Symbol, I honestly doubt it's worth all the trouble that brings with it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth all the trouble that brings with it

You always start with Symbol for looking up things and then call .as_str() on it, the only thing that's more complex is inserting (you have to call Symbol::intern("...")) but that happens only once.


/// The random number generator used for resolving non-determinism.
/// Needs to be queried by ptr_to_int, hence needs interior mutability.
pub(crate) rng: RefCell<StdRng>,
}

Expand All @@ -85,7 +89,34 @@ impl MemoryExtra {
} else {
None
};
MemoryExtra { stacked_borrows, intptrcast: Default::default(), rng: RefCell::new(rng) }
MemoryExtra {
stacked_borrows,
intptrcast: Default::default(),
extern_statics: HashMap::default(),
rng: RefCell::new(rng),
}
}

/// Sets up the "extern statics" for this machine.
pub fn init_extern_statics<'mir, 'tcx>(
this: &mut MiriEvalContext<'mir, 'tcx>,
) -> InterpResult<'tcx> {
match this.tcx.sess.target.target.target_os.as_str() {
"linux" => {
// "__cxa_thread_atexit_impl"
// This should be all-zero, pointer-sized.
let layout = this.layout_of(this.tcx.types.usize)?;
let place = this.allocate(layout, MiriMemoryKind::Machine.into());
this.write_scalar(Scalar::from_machine_usize(0, &*this.tcx), place.into())?;
this.memory
.extra
.extern_statics
.insert("__cxa_thread_atexit_impl", place.ptr.assert_ptr().alloc_id)
.unwrap_none();
}
_ => {} // No "extern statics" supported on this platform
}
Ok(())
}
}

Expand Down Expand Up @@ -267,32 +298,29 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for Evaluator<'tcx> {
Ok(())
}

fn find_foreign_static(
tcx: TyCtxt<'tcx>,
def_id: DefId,
) -> InterpResult<'tcx, Cow<'tcx, Allocation>> {
fn canonical_alloc_id(mem: &Memory<'mir, 'tcx, Self>, id: AllocId) -> AllocId {
let tcx = mem.tcx;
// Figure out if this is an extern static, and if yes, which one.
let def_id = match tcx.alloc_map.lock().get(id) {
Some(GlobalAlloc::Static(def_id)) if tcx.is_foreign_item(def_id) => def_id,
_ => {
// No need to canonicalize anything.
return id;
}
};
let attrs = tcx.get_attrs(def_id);
let link_name = match attr::first_attr_value_str_by_name(&attrs, sym::link_name) {
Some(name) => name.as_str(),
None => tcx.item_name(def_id).as_str(),
};

let alloc = match &*link_name {
"__cxa_thread_atexit_impl" => {
// This should be all-zero, pointer-sized.
let size = tcx.data_layout.pointer_size;
let data = vec![0; size.bytes() as usize];
Allocation::from_bytes(&data, tcx.data_layout.pointer_align.abi)
}
_ => throw_unsup_format!("can't access foreign static: {}", link_name),
};
Ok(Cow::Owned(alloc))
}

#[inline(always)]
fn before_terminator(_ecx: &mut InterpCx<'mir, 'tcx, Self>) -> InterpResult<'tcx> {
// We are not interested in detecting loops.
Ok(())
// Check if we know this one.
if let Some(canonical_id) = mem.extra.extern_statics.get(&*link_name) {
trace!("canonical_alloc_id: {:?} ({}) -> {:?}", id, link_name, canonical_id);
*canonical_id
} else {
// Return original id; `Memory::get_static_alloc` will throw an error.
id
}
}

fn init_allocation_extra<'b>(
Expand Down Expand Up @@ -433,7 +461,7 @@ impl MayLeak for MiriMemoryKind {
use self::MiriMemoryKind::*;
match self {
Rust | C | WinHeap => false,
Env | Static => true,
Machine | Static => true,
}
}
}
6 changes: 3 additions & 3 deletions src/shims/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ fn alloc_env_var_as_c_str<'mir, 'tcx>(
let mut name_osstring = name.to_os_string();
name_osstring.push("=");
name_osstring.push(value);
ecx.alloc_os_str_as_c_str(name_osstring.as_os_str(), MiriMemoryKind::Env.into())
ecx.alloc_os_str_as_c_str(name_osstring.as_os_str(), MiriMemoryKind::Machine.into())
}

impl<'mir, 'tcx> EvalContextExt<'mir, 'tcx> for crate::MiriEvalContext<'mir, 'tcx> {}
Expand Down Expand Up @@ -80,7 +80,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
let var_ptr = alloc_env_var_as_c_str(&name, &value, &mut this);
if let Some(var) = this.machine.env_vars.map.insert(name.to_owned(), var_ptr) {
this.memory
.deallocate(var, None, MiriMemoryKind::Env.into())?;
.deallocate(var, None, MiriMemoryKind::Machine.into())?;
}
Ok(0)
} else {
Expand All @@ -102,7 +102,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
if let Some(old) = success {
if let Some(var) = old {
this.memory
.deallocate(var, None, MiriMemoryKind::Env.into())?;
.deallocate(var, None, MiriMemoryKind::Machine.into())?;
}
Ok(0)
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/shims/panic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx

// First arg: Message.
let msg = msg.description();
let msg = this.allocate_str(msg, MiriMemoryKind::Env.into());
let msg = this.allocate_str(msg, MiriMemoryKind::Machine.into());

// Call the lang item.
let panic = this.tcx.lang_items().panic_fn().unwrap();
Expand Down
2 changes: 1 addition & 1 deletion src/stacked_borrows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ impl Stacks {
// Thus we call `static_base_ptr` such that the global pointers get the same tag
// as what we use here.
// The base pointer is not unique, so the base permission is `SharedReadWrite`.
MemoryKind::Machine(MiriMemoryKind::Static) =>
MemoryKind::Machine(MiriMemoryKind::Static) | MemoryKind::Machine(MiriMemoryKind::Machine) =>
(extra.borrow_mut().static_base_ptr(id), Permission::SharedReadWrite),
// Everything else we handle entirely untagged for now.
// FIXME: experiment with more precise tracking.
Expand Down