///|
pub(all) struct VmTelemetry {
operation_id : Int
operation : String
source_name : String
succeeded : Bool
diagnostic_count : Int
steps : Int
duration_milliseconds : Double
output_lines : Int
output_characters : Int
loaded_modules : Int
allocated_objects : Int
live_fibers : Int
foreign_calls : Int
peak_call_depth : Int
last_gc_reclaimed_objects : Int
}
///|
pub struct ObservedVm {
vm : Vm
history_limit : Int
history : Array[VmTelemetry]
observers : Array[(VmTelemetry) -> Unit]
mut next_operation_id : Int
}
///|
pub fn ObservedVm::new(vm : Vm, history_limit : Int) -> ObservedVm {
{
vm,
history_limit: if history_limit > 0 {
history_limit
} else {
1
},
history: [],
observers: [],
next_operation_id: 1,
}
}
///|
pub fn ObservedVm::subscribe(
self : ObservedVm,
observer : (VmTelemetry) -> Unit,
) -> Unit {
self.observers.push(observer)
}
///|
fn ObservedVm::record(
self : ObservedVm,
operation : String,
source_name : String,
started : @bench.Timestamp,
result : ProgramRunResult,
) -> ProgramRunResult {
let context = self.vm.runtime()
let (succeeded, diagnostic_count, output_lines) = match result {
ProgramCompleted(_, output) => (true, 0, output.length())
ProgramFailed(issues, output) => (false, issues.length(), output.length())
}
let last_gc_reclaimed_objects = match self.vm.last_gc_stats {
Some(stats) => stats.reclaimed_objects
None => 0
}
let telemetry : VmTelemetry = {
operation_id: self.next_operation_id,
operation,
source_name,
succeeded,
diagnostic_count,
steps: context.steps,
duration_milliseconds: @bench.monotonic_clock_end(started) / 1000000.0,
output_lines,
output_characters: context.output_characters,
loaded_modules: context.modules.length(),
allocated_objects: context.allocated_objects(),
live_fibers: context.fibers.length() - context.free_fibers.length(),
foreign_calls: context.foreign_calls,
peak_call_depth: context.peak_call_depth,
last_gc_reclaimed_objects,
}
self.next_operation_id = self.next_operation_id + 1
self.history.push(telemetry)
while self.history.length() > self.history_limit {
ignore(self.history.remove(0))
}
for observer in self.observers {
observer(telemetry)
}
result
}
///|
pub fn ObservedVm::interpret(
self : ObservedVm,
source_name : String,
source : String,
) -> ProgramRunResult {
let started = @bench.monotonic_clock_start()
let result = self.vm.interpret(source_name, source)
self.record("interpret", source_name, started, result)
}
///|
pub fn ObservedVm::read(
self : ObservedVm,
module_name : String,
variable : String,
) -> ProgramRunResult {
let started = @bench.monotonic_clock_start()
let result = self.vm.read(module_name, variable)
self.record("read", module_name, started, result)
}
///|
pub fn ObservedVm::call(
self : ObservedVm,
handle : VmHandle,
arguments : Array[Value],
) -> ProgramRunResult {
let started = @bench.monotonic_clock_start()
let result = self.vm.call(handle, arguments)
self.record("call", "", started, result)
}
///|
pub fn ObservedVm::reload_modules(
self : ObservedVm,
replacements : Array[ModuleSource],
preserve : Array[ReloadStateSelection],
) -> ProgramRunResult {
let started = @bench.monotonic_clock_start()
let result = self.vm.reload_modules(replacements, preserve)
self.record("reload", "", started, result)
}
///|
pub fn ObservedVm::capture(
self : ObservedVm,
module_name : String,
variable : String,
) -> VmHandleResult {
self.vm.capture(module_name, variable)
}
///|
pub fn ObservedVm::release(self : ObservedVm, handle : VmHandle) -> Bool {
self.vm.release(handle)
}
///|
pub fn ObservedVm::history(self : ObservedVm) -> Array[VmTelemetry] {
self.history.copy()
}
///|
pub fn ObservedVm::underlying(self : ObservedVm) -> Vm {
self.vm
}
///|
pub fn ObservedVm::close(self : ObservedVm) -> Unit {
self.vm.close()
}
///|
/// Export aggregate counters in Prometheus text exposition format. The prefix
/// is normalized to letters, digits, underscores, and colons.
pub fn telemetry_prometheus(
history : Array[VmTelemetry],
prefix : String,
) -> String {
let normalized : Array[Char] = []
for character in prefix {
let code = character.to_int()
if (code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
code == 95 ||
code == 58 {
normalized.push(character)
} else {
normalized.push('_')
}
}
let name = if normalized.is_empty() {
"moonwren"
} else {
String::from_array(normalized)
}
let mut successes = 0
let mut failures = 0
let mut steps = 0
let mut foreign_calls = 0
let mut duration = 0.0
for item in history {
if item.succeeded {
successes = successes + 1
} else {
failures = failures + 1
}
steps = steps + item.steps
foreign_calls = foreign_calls + item.foreign_calls
duration = duration + item.duration_milliseconds
}
"# TYPE " +
name +
"_operations_total counter\n" +
name +
"_operations_total{result=\"success\"} " +
successes.to_string() +
"\n" +
name +
"_operations_total{result=\"failure\"} " +
failures.to_string() +
"\n" +
"# TYPE " +
name +
"_steps_total counter\n" +
name +
"_steps_total " +
steps.to_string() +
"\n" +
"# TYPE " +
name +
"_foreign_calls_total counter\n" +
name +
"_foreign_calls_total " +
foreign_calls.to_string() +
"\n" +
"# TYPE " +
name +
"_duration_milliseconds_total counter\n" +
name +
"_duration_milliseconds_total " +
duration.to_string() +
"\n"
}
///|
fn telemetry_json_escape(text : String) -> String {
text
.replace_all(old="\\", new="\\\\")
.replace_all(old="\"", new="\\\"")
.replace_all(old="\n", new="\\n")
.replace_all(old="\r", new="\\r")
}
///|
/// Export complete-duration events accepted by Chrome, Perfetto, and other
/// trace-event viewers.
pub fn telemetry_chrome_trace(history : Array[VmTelemetry]) -> String {
let events : Array[String] = []
let mut timestamp_microseconds = 0.0
for item in history {
let duration_microseconds = item.duration_milliseconds * 1000.0
let status = if item.succeeded { "success" } else { "failure" }
events.push(
"{\"name\":\"" +
telemetry_json_escape(item.operation) +
"\",\"cat\":\"moonwren.vm\",\"ph\":\"X\",\"ts\":" +
timestamp_microseconds.to_string() +
",\"dur\":" +
duration_microseconds.to_string() +
",\"pid\":1,\"tid\":1,\"args\":{\"source\":\"" +
telemetry_json_escape(item.source_name) +
"\",\"result\":\"" +
status +
"\",\"steps\":" +
item.steps.to_string() +
",\"objects\":" +
item.allocated_objects.to_string() +
"}}",
)
timestamp_microseconds = timestamp_microseconds + duration_microseconds
}
"{\"traceEvents\":[" + events.join(",") + "]}"
}