This replaces the OpCode enum with a new Op enum which is guaranteed to fit in a
single byte. Instead of carrying enum variants with data, every variant that has
runtime data encodes it into the `Vec<u8>` that a `Chunk` now carries.
This has several advantages:
* Less stack space is required at runtime, and fewer allocations are required
while compiling.
* The OpCode doesn't need to carry "weird" special-cased data variants anymore.
* It is faster (albeit, not by much). On my laptop, results consistently look
approximately like this:
Benchmark 1: ./before -E '(import <nixpkgs> {}).firefox.outPath' --log-level ERROR --no-warnings
Time (mean ± σ): 8.224 s ± 0.272 s [User: 7.149 s, System: 0.688 s]
Range (min … max): 7.759 s … 8.583 s 10 runs
Benchmark 2: ./after -E '(import <nixpkgs> {}).firefox.outPath' --log-level ERROR --no-warnings
Time (mean ± σ): 8.000 s ± 0.198 s [User: 7.036 s, System: 0.633 s]
Range (min … max): 7.718 s … 8.334 s 10 runs
See notes below for why the performance impact might be less than expected.
* It is faster while at the same time dropping some optimisations we previously
performed.
This has several disadvantages:
* The code is closer to how one would write it in C or Go.
* Bit shifting!
* There is (for now) slightly more code than before.
On performance I have the following thoughts at the moment:
In order to prepare for adding GC, there's a couple of places in Tvix where I'd
like to fence off certain kinds of complexity (such as mutating bytecode, which,
for various reaons, also has to be part of data that is subject to GC). With
this change, we can drop optimisations like retroactively modifying existing
bytecode and *still* achieve better performance than before.
I believe that this is currently worth it to pave the way for changes that are
more significant for performance.
In general this also opens other avenues of optimisation: For example, we can
profile which argument sizes actually exist and remove the copy overhead of
varint decoding (which does show up in profiles) by using more adequately sized
types for, e.g., constant indices.
Known regressions:
* Op::Constant is no longer printing its values in disassembly (this can be
fixed, I just didn't get around to it, will do separately).
Change-Id: Id9b3a4254623a45de03069dbdb70b8349e976743
Reviewed-on: https://cl.tvl.fyi/c/depot/+/12191
Tested-by: BuildkiteCI
Reviewed-by: flokli <flokli@flokli.de>
322 lines
10 KiB
Rust
322 lines
10 KiB
Rust
//! Implements traits for things that wish to observe internal state
|
|
//! changes of tvix-eval.
|
|
//!
|
|
//! This can be used to gain insights from compilation, to trace the
|
|
//! runtime, and so on.
|
|
//!
|
|
//! All methods are optional, that is, observers can implement only
|
|
/// what they are interested in observing.
|
|
use std::io::Write;
|
|
use std::rc::Rc;
|
|
use std::time::Instant;
|
|
use tabwriter::TabWriter;
|
|
|
|
use crate::chunk::Chunk;
|
|
use crate::generators::VMRequest;
|
|
use crate::opcode::{CodeIdx, Op};
|
|
use crate::value::Lambda;
|
|
use crate::SourceCode;
|
|
use crate::Value;
|
|
|
|
/// Implemented by types that wish to observe internal happenings of
|
|
/// the Tvix compiler.
|
|
pub trait CompilerObserver {
|
|
/// Called when the compiler finishes compilation of the top-level
|
|
/// of an expression (usually the root Nix expression of a file).
|
|
fn observe_compiled_toplevel(&mut self, _: &Rc<Lambda>) {}
|
|
|
|
/// Called when the compiler finishes compilation of a
|
|
/// user-defined function.
|
|
///
|
|
/// Note that in Nix there are only single argument functions, so
|
|
/// in an expression like `a: b: c: ...` this method will be
|
|
/// called three times.
|
|
fn observe_compiled_lambda(&mut self, _: &Rc<Lambda>) {}
|
|
|
|
/// Called when the compiler finishes compilation of a thunk.
|
|
fn observe_compiled_thunk(&mut self, _: &Rc<Lambda>) {}
|
|
}
|
|
|
|
/// Implemented by types that wish to observe internal happenings of
|
|
/// the Tvix virtual machine at runtime.
|
|
pub trait RuntimeObserver {
|
|
/// Called when the runtime enters a new call frame.
|
|
fn observe_enter_call_frame(&mut self, _arg_count: usize, _: &Rc<Lambda>, _call_depth: usize) {}
|
|
|
|
/// Called when the runtime exits a call frame.
|
|
fn observe_exit_call_frame(&mut self, _frame_at: usize, _stack: &[Value]) {}
|
|
|
|
/// Called when the runtime suspends a call frame.
|
|
fn observe_suspend_call_frame(&mut self, _frame_at: usize, _stack: &[Value]) {}
|
|
|
|
/// Called when the runtime enters a generator frame.
|
|
fn observe_enter_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
|
|
|
|
/// Called when the runtime exits a generator frame.
|
|
fn observe_exit_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
|
|
|
|
/// Called when the runtime suspends a generator frame.
|
|
fn observe_suspend_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
|
|
|
|
/// Called when a generator requests an action from the VM.
|
|
fn observe_generator_request(&mut self, _name: &str, _msg: &VMRequest) {}
|
|
|
|
/// Called when the runtime replaces the current call frame for a
|
|
/// tail call.
|
|
fn observe_tail_call(&mut self, _frame_at: usize, _: &Rc<Lambda>) {}
|
|
|
|
/// Called when the runtime enters a builtin.
|
|
fn observe_enter_builtin(&mut self, _name: &'static str) {}
|
|
|
|
/// Called when the runtime exits a builtin.
|
|
fn observe_exit_builtin(&mut self, _name: &'static str, _stack: &[Value]) {}
|
|
|
|
/// Called when the runtime *begins* executing an instruction. The
|
|
/// provided stack is the state at the beginning of the operation.
|
|
fn observe_execute_op(&mut self, _ip: CodeIdx, _: &Op, _: &[Value]) {}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct NoOpObserver {}
|
|
|
|
impl CompilerObserver for NoOpObserver {}
|
|
impl RuntimeObserver for NoOpObserver {}
|
|
|
|
/// An observer that prints disassembled chunk information to its
|
|
/// internal writer whenwever the compiler emits a toplevel function,
|
|
/// closure or thunk.
|
|
pub struct DisassemblingObserver<W: Write> {
|
|
source: SourceCode,
|
|
writer: TabWriter<W>,
|
|
}
|
|
|
|
impl<W: Write> DisassemblingObserver<W> {
|
|
pub fn new(source: SourceCode, writer: W) -> Self {
|
|
Self {
|
|
source,
|
|
writer: TabWriter::new(writer),
|
|
}
|
|
}
|
|
|
|
fn lambda_header(&mut self, kind: &str, lambda: &Rc<Lambda>) {
|
|
let _ = writeln!(
|
|
&mut self.writer,
|
|
"=== compiled {} @ {:p} ({} ops) ===",
|
|
kind,
|
|
*lambda,
|
|
lambda.chunk.code.len()
|
|
);
|
|
}
|
|
|
|
fn disassemble_chunk(&mut self, chunk: &Chunk) {
|
|
// calculate width of the widest address in the chunk
|
|
let width = format!("{:#x}", chunk.code.len() - 1).len();
|
|
|
|
let mut idx = 0;
|
|
while idx < chunk.code.len() {
|
|
let size = chunk
|
|
.disassemble_op(&mut self.writer, &self.source, width, CodeIdx(idx))
|
|
.expect("writing debug output should work");
|
|
idx += size;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<W: Write> CompilerObserver for DisassemblingObserver<W> {
|
|
fn observe_compiled_toplevel(&mut self, lambda: &Rc<Lambda>) {
|
|
self.lambda_header("toplevel", lambda);
|
|
self.disassemble_chunk(&lambda.chunk);
|
|
let _ = self.writer.flush();
|
|
}
|
|
|
|
fn observe_compiled_lambda(&mut self, lambda: &Rc<Lambda>) {
|
|
self.lambda_header("lambda", lambda);
|
|
self.disassemble_chunk(&lambda.chunk);
|
|
let _ = self.writer.flush();
|
|
}
|
|
|
|
fn observe_compiled_thunk(&mut self, lambda: &Rc<Lambda>) {
|
|
self.lambda_header("thunk", lambda);
|
|
self.disassemble_chunk(&lambda.chunk);
|
|
let _ = self.writer.flush();
|
|
}
|
|
}
|
|
|
|
/// An observer that collects a textual representation of an entire
|
|
/// runtime execution.
|
|
pub struct TracingObserver<W: Write> {
|
|
// If timing is enabled, contains the timestamp of the last-emitted trace event
|
|
last_event: Option<Instant>,
|
|
writer: TabWriter<W>,
|
|
}
|
|
|
|
impl<W: Write> TracingObserver<W> {
|
|
pub fn new(writer: W) -> Self {
|
|
Self {
|
|
last_event: None,
|
|
writer: TabWriter::new(writer),
|
|
}
|
|
}
|
|
|
|
/// Write the time of each runtime event, relative to when this method is called
|
|
pub fn enable_timing(&mut self) {
|
|
self.last_event = Some(Instant::now());
|
|
}
|
|
|
|
fn maybe_write_time(&mut self) {
|
|
if let Some(last_event) = &mut self.last_event {
|
|
let _ = write!(&mut self.writer, "+{}ns\t", last_event.elapsed().as_nanos());
|
|
*last_event = Instant::now();
|
|
}
|
|
}
|
|
|
|
fn write_value(&mut self, val: &Value) {
|
|
let _ = match val {
|
|
// Potentially large types which we only want to print
|
|
// the type of (and avoid recursing).
|
|
Value::List(l) => write!(&mut self.writer, "list[{}] ", l.len()),
|
|
Value::Attrs(a) => write!(&mut self.writer, "attrs[{}] ", a.len()),
|
|
Value::Thunk(t) if t.is_evaluated() => {
|
|
self.write_value(&t.value());
|
|
Ok(())
|
|
}
|
|
|
|
// For other value types, defer to the standard value printer.
|
|
_ => write!(&mut self.writer, "{} ", val),
|
|
};
|
|
}
|
|
|
|
fn write_stack(&mut self, stack: &[Value]) {
|
|
let _ = write!(&mut self.writer, "[ ");
|
|
|
|
// Print out a maximum of 6 values from the top of the stack,
|
|
// before abbreviating it to `...`.
|
|
for (i, val) in stack.iter().rev().enumerate() {
|
|
if i == 6 {
|
|
let _ = write!(&mut self.writer, "...");
|
|
break;
|
|
}
|
|
|
|
self.write_value(val);
|
|
}
|
|
|
|
let _ = writeln!(&mut self.writer, "]");
|
|
}
|
|
}
|
|
|
|
impl<W: Write> RuntimeObserver for TracingObserver<W> {
|
|
fn observe_enter_call_frame(
|
|
&mut self,
|
|
arg_count: usize,
|
|
lambda: &Rc<Lambda>,
|
|
call_depth: usize,
|
|
) {
|
|
self.maybe_write_time();
|
|
|
|
let _ = write!(&mut self.writer, "=== entering ");
|
|
|
|
let _ = if arg_count == 0 {
|
|
write!(&mut self.writer, "thunk ")
|
|
} else {
|
|
write!(&mut self.writer, "closure ")
|
|
};
|
|
|
|
if let Some(name) = &lambda.name {
|
|
let _ = write!(&mut self.writer, "'{}' ", name);
|
|
}
|
|
|
|
let _ = writeln!(
|
|
&mut self.writer,
|
|
"in frame[{}] @ {:p} ===",
|
|
call_depth, *lambda
|
|
);
|
|
}
|
|
|
|
/// Called when the runtime exits a call frame.
|
|
fn observe_exit_call_frame(&mut self, frame_at: usize, stack: &[Value]) {
|
|
self.maybe_write_time();
|
|
let _ = write!(&mut self.writer, "=== exiting frame {} ===\t ", frame_at);
|
|
self.write_stack(stack);
|
|
}
|
|
|
|
fn observe_suspend_call_frame(&mut self, frame_at: usize, stack: &[Value]) {
|
|
self.maybe_write_time();
|
|
let _ = write!(&mut self.writer, "=== suspending frame {} ===\t", frame_at);
|
|
|
|
self.write_stack(stack);
|
|
}
|
|
|
|
fn observe_enter_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
|
|
self.maybe_write_time();
|
|
let _ = write!(
|
|
&mut self.writer,
|
|
"=== entering generator frame '{}' [{}] ===\t",
|
|
name, frame_at,
|
|
);
|
|
|
|
self.write_stack(stack);
|
|
}
|
|
|
|
fn observe_exit_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
|
|
self.maybe_write_time();
|
|
let _ = write!(
|
|
&mut self.writer,
|
|
"=== exiting generator '{}' [{}] ===\t",
|
|
name, frame_at
|
|
);
|
|
|
|
self.write_stack(stack);
|
|
}
|
|
|
|
fn observe_suspend_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
|
|
self.maybe_write_time();
|
|
let _ = write!(
|
|
&mut self.writer,
|
|
"=== suspending generator '{}' [{}] ===\t",
|
|
name, frame_at
|
|
);
|
|
|
|
self.write_stack(stack);
|
|
}
|
|
|
|
fn observe_generator_request(&mut self, name: &str, msg: &VMRequest) {
|
|
self.maybe_write_time();
|
|
let _ = writeln!(
|
|
&mut self.writer,
|
|
"=== generator '{}' requested {} ===",
|
|
name, msg
|
|
);
|
|
}
|
|
|
|
fn observe_enter_builtin(&mut self, name: &'static str) {
|
|
self.maybe_write_time();
|
|
let _ = writeln!(&mut self.writer, "=== entering builtin {} ===", name);
|
|
}
|
|
|
|
fn observe_exit_builtin(&mut self, name: &'static str, stack: &[Value]) {
|
|
self.maybe_write_time();
|
|
let _ = write!(&mut self.writer, "=== exiting builtin {} ===\t", name);
|
|
self.write_stack(stack);
|
|
}
|
|
|
|
fn observe_tail_call(&mut self, frame_at: usize, lambda: &Rc<Lambda>) {
|
|
self.maybe_write_time();
|
|
let _ = writeln!(
|
|
&mut self.writer,
|
|
"=== tail-calling {:p} in frame[{}] ===",
|
|
*lambda, frame_at
|
|
);
|
|
}
|
|
|
|
fn observe_execute_op(&mut self, ip: CodeIdx, op: &Op, stack: &[Value]) {
|
|
self.maybe_write_time();
|
|
let _ = write!(&mut self.writer, "{:04} {:?}\t", ip.0, op);
|
|
self.write_stack(stack);
|
|
}
|
|
}
|
|
|
|
impl<W: Write> Drop for TracingObserver<W> {
|
|
fn drop(&mut self) {
|
|
let _ = self.writer.flush();
|
|
}
|
|
}
|