From 6b333a6193f12a6a5489b347c834cad4663493f6 Mon Sep 17 00:00:00 2001 From: Glyn Normington Date: Wed, 14 Jun 2017 12:54:13 +0100 Subject: [PATCH] Print heap histogram Also: * Avoid unhelpful compiler warnings * Capture common code in a macro Note: it would have been nice to alias the closure type, but https://github.com/rust-lang/rfcs/pull/1733 is not yet implemented and macros can't cope (https://github.com/rust-lang/rust/issues/24010). --- src/agentcontroller/controller.rs | 7 +- src/agentcontroller/heaphistogram.rs | 35 +++- src/agentcontroller/kill.rs | 3 +- src/env.rs | 251 ++++++++++++++++++--------- src/heap.rs | 119 +++++++++++++ src/lib.rs | 1 + 6 files changed, 329 insertions(+), 87 deletions(-) create mode 100644 src/heap.rs diff --git a/src/agentcontroller/controller.rs b/src/agentcontroller/controller.rs index 80237b9..00f50f6 100644 --- a/src/agentcontroller/controller.rs +++ b/src/agentcontroller/controller.rs @@ -15,6 +15,7 @@ */ pub struct AgentController<'a> { + #[allow(dead_code)] // TODO: revisit this once port is complete jvmti: ::env::JvmTiEnv, heuristic: Box, actions: Vec> @@ -126,9 +127,9 @@ mod tests { ac.on_oom(dummy_jni_env(), 0); } - unsafe extern "C" fn test_get_env(vm: *mut ::jvmti::JavaVM, - penv: *mut *mut ::std::os::raw::c_void, - version: ::jvmti::jint) + unsafe extern "C" fn test_get_env(_: *mut ::jvmti::JavaVM, + _: *mut *mut ::std::os::raw::c_void, + _: ::jvmti::jint) -> ::jvmti::jint { 0 } diff --git a/src/agentcontroller/heaphistogram.rs b/src/agentcontroller/heaphistogram.rs index 2541775..ee4a78b 100644 --- a/src/agentcontroller/heaphistogram.rs +++ b/src/agentcontroller/heaphistogram.rs @@ -14,19 +14,48 @@ * limitations under the License. */ +use env::JvmTI; +use std::io::Write; +use std::io::stdout; +use heap::Tagger; +use heap::Tag; +use heap::Stats; +use heap::Record; +use heap::Print; + pub struct HeapHistogram { jvmti: ::env::JvmTiEnv, } impl HeapHistogram { - pub fn new(jvmti: ::env::JvmTiEnv) -> Result { + pub fn new(mut jvmti: ::env::JvmTiEnv) -> Result { + jvmti.enable_object_tagging()?; Ok(Self { - jvmti: jvmti + jvmti: jvmti, }) } + + pub fn print(&self, writer: &mut Write) { + let mut tagger = Tagger::new(); + + // Tag all loaded classes so we can determine each object's class signature during heap traversal. + self.jvmti.tag_loaded_classes(&mut tagger); + + let mut heap_stats = Stats::new(); + + // Traverse the live heap and add objects to the heap stats. + self.jvmti.traverse_live_heap(|class_tag: ::jvmti::jlong, size: ::jvmti::jlong| { + if let Some(sig) = tagger.class_signature(class_tag) { + heap_stats.recordObject(sig, size); + } + }); + + heap_stats.print(writer); + } } impl super::Action for HeapHistogram { - fn on_oom(&self, jni_env: ::env::JniEnv, resource_exhaustion_flags: ::jvmti::jint) { + fn on_oom(&self, _: ::env::JniEnv, _: ::jvmti::jint) { + self.print(&mut stdout()); } } diff --git a/src/agentcontroller/kill.rs b/src/agentcontroller/kill.rs index 22037ac..0982689 100644 --- a/src/agentcontroller/kill.rs +++ b/src/agentcontroller/kill.rs @@ -30,6 +30,7 @@ impl Kill { } } + #[cfg(test)] pub fn setSignal(&mut self, signal: c_int) { self.signal = signal; } @@ -93,7 +94,7 @@ mod tests { signal::SaFlags::empty(), signal::SigSet::empty()); unsafe { - signal::sigaction(signal::SIGUSR1, &sig_action); + signal::sigaction(signal::SIGUSR1, &sig_action).unwrap(); } } } diff --git a/src/env.rs b/src/env.rs index cf8b0b5..653f055 100644 --- a/src/env.rs +++ b/src/env.rs @@ -15,17 +15,26 @@ */ use std::mem::size_of; +use std::mem::transmute; use ::std::ptr; use ::std::ffi::CString; +use ::std::ffi::CStr; use ::std::sync::Mutex; use ::jvmti::jvmtiEnv; use ::jvmti::jrawMonitorID; +use ::heap::Tag; pub trait JvmTI { + // TODO: rework the following methods not to return values since they only ever return Ok(()) or panic. fn create_raw_monitor(&mut self, name: String, monitor: &Mutex) -> Result<(), ::jvmti::jint>; fn raw_monitor_enter(&mut self, monitor: &Mutex) -> Result<(), ::jvmti::jint>; fn raw_monitor_exit(&mut self, monitor: &Mutex) -> Result<(), ::jvmti::jint>; fn on_resource_exhausted(&mut self, callback: FnResourceExhausted) -> Result<(), ::jvmti::jint>; + fn enable_object_tagging(&mut self) -> Result<(), ::jvmti::jint>; + fn tag_loaded_classes(&self, tagger: &mut Tag); + + // Restriction: traverse_live_heap may be called at most once in the lifetime of a JVM. + fn traverse_live_heap(&self, closure: F) where F: FnMut(::jvmti::jlong, ::jvmti::jlong); } pub struct RawMonitorId { @@ -66,43 +75,37 @@ impl JvmTiEnv { } } -impl JvmTI for JvmTiEnv { - fn create_raw_monitor(&mut self, name: String, monitor: &Mutex) -> Result<(), ::jvmti::jint> { +macro_rules! jvmtifn ( + ($r:expr, $f:ident, $($arg:tt)*) => { { let rc; + #[allow(unused_unsafe)] // suppress warning if used inside unsafe block unsafe { - let create_raw_monitor_fn = (**self.jvmti).CreateRawMonitor.unwrap(); - rc = create_raw_monitor_fn(self.jvmti, CString::new(name).unwrap().into_raw(), monitor.lock().unwrap().id); + let fnc = (**$r).$f.unwrap(); + rc = fnc($r, $($arg)*); } if rc != ::jvmti::jvmtiError::JVMTI_ERROR_NONE { - eprintln!("ERROR: CreateRawMonitor failed: {:?}", rc); - return Err(::jvmti::JNI_ERR); + eprintln!("ERROR: JVMTI {} failed: {:?}", stringify!($f), rc); + panic!(::jvmti::JNI_ERR); } + } } +); + +// Pick a suitable object tag mask greater than tags used to tag classes. +const TAG_VISITED_MASK: ::jvmti::jlong = 1 << 31; + +impl JvmTI for JvmTiEnv { + fn create_raw_monitor(&mut self, name: String, monitor: &Mutex) -> Result<(), ::jvmti::jint> { + jvmtifn!(self.jvmti, CreateRawMonitor, CString::new(name).unwrap().into_raw(), monitor.lock().unwrap().id); Ok(()) } fn raw_monitor_enter(&mut self, monitor: &Mutex) -> Result<(), ::jvmti::jint> { - let rc; - unsafe { - let raw_monitor_enter_fn = (**self.jvmti).RawMonitorEnter.unwrap(); - rc = raw_monitor_enter_fn(self.jvmti, *monitor.lock().unwrap().id); - } - if rc != ::jvmti::jvmtiError::JVMTI_ERROR_NONE { - eprintln!("ERROR: RawMonitorEnter failed: {:?}", rc); - return Err(::jvmti::JNI_ERR); - } + jvmtifn!(self.jvmti, RawMonitorEnter, *monitor.lock().unwrap().id); Ok(()) } fn raw_monitor_exit(&mut self, monitor: &Mutex) -> Result<(), ::jvmti::jint> { - let rc; - unsafe { - let raw_monitor_exit_fn = (**self.jvmti).RawMonitorExit.unwrap(); - rc = raw_monitor_exit_fn(self.jvmti, *monitor.lock().unwrap().id); - } - if rc != ::jvmti::jvmtiError::JVMTI_ERROR_NONE { - eprintln!("ERROR: RawMonitorExit failed: {:?}", rc); - return Err(::jvmti::JNI_ERR); - } + jvmtifn!(self.jvmti, RawMonitorExit, *monitor.lock().unwrap().id); Ok(()) } @@ -111,65 +114,125 @@ impl JvmTI for JvmTiEnv { EVENT_CALLBACKS.resource_exhausted = Some(callback); } - let rc; - unsafe { - let set_event_callbacks_fn = (**self.jvmti).SetEventCallbacks.unwrap(); - let callbacks = ::jvmti::jvmtiEventCallbacks { - VMInit: None, - VMDeath: None, - ThreadStart: None, - ThreadEnd: None, - ClassFileLoadHook: None, - ClassLoad: None, - ClassPrepare: None, - VMStart: None, - Exception: None, - ExceptionCatch: None, - SingleStep: None, - FramePop: None, - Breakpoint: None, - FieldAccess: None, - FieldModification: None, - MethodEntry: None, - MethodExit: None, - NativeMethodBind: None, - CompiledMethodLoad: None, - CompiledMethodUnload: None, - DynamicCodeGenerated: None, - DataDumpRequest: None, - reserved72: None, - MonitorWait: None, - MonitorWaited: None, - MonitorContendedEnter: None, - MonitorContendedEntered: None, - reserved77: None, - reserved78: None, - reserved79: None, - ResourceExhausted: Some(resource_exhausted), - GarbageCollectionStart: None, - GarbageCollectionFinish: None, - ObjectFree: None, - VMObjectAlloc: None - }; - rc = set_event_callbacks_fn(self.jvmti, &callbacks, size_of::<::jvmti::jvmtiEventCallbacks>() as i32); - } - if rc != ::jvmti::jvmtiError::JVMTI_ERROR_NONE { - eprintln!("ERROR: SetEventCallbacks failed: {:?}", rc); - return Err(::jvmti::JNI_ERR); - } + let callbacks = ::jvmti::jvmtiEventCallbacks { + VMInit: None, + VMDeath: None, + ThreadStart: None, + ThreadEnd: None, + ClassFileLoadHook: None, + ClassLoad: None, + ClassPrepare: None, + VMStart: None, + Exception: None, + ExceptionCatch: None, + SingleStep: None, + FramePop: None, + Breakpoint: None, + FieldAccess: None, + FieldModification: None, + MethodEntry: None, + MethodExit: None, + NativeMethodBind: None, + CompiledMethodLoad: None, + CompiledMethodUnload: None, + DynamicCodeGenerated: None, + DataDumpRequest: None, + reserved72: None, + MonitorWait: None, + MonitorWaited: None, + MonitorContendedEnter: None, + MonitorContendedEntered: None, + reserved77: None, + reserved78: None, + reserved79: None, + ResourceExhausted: Some(resource_exhausted), + GarbageCollectionStart: None, + GarbageCollectionFinish: None, + ObjectFree: None, + VMObjectAlloc: None + }; + jvmtifn!(self.jvmti, SetEventCallbacks, &callbacks, size_of::<::jvmti::jvmtiEventCallbacks>() as i32); - let rc; - unsafe { - let set_event_notification_mode_fn = (**self.jvmti).SetEventNotificationMode.unwrap(); - rc = set_event_notification_mode_fn(self.jvmti, ::jvmti::jvmtiEventMode::JVMTI_ENABLE, ::jvmti::jvmtiEvent::JVMTI_EVENT_RESOURCE_EXHAUSTED, ::std::ptr::null_mut()); - } - if rc != ::jvmti::jvmtiError::JVMTI_ERROR_NONE { - eprintln!("ERROR: SetEventNotificationMode failed: {:?}", rc); - return Err(::jvmti::JNI_ERR); - } + jvmtifn!(self.jvmti, SetEventNotificationMode, ::jvmti::jvmtiEventMode::JVMTI_ENABLE, ::jvmti::jvmtiEvent::JVMTI_EVENT_RESOURCE_EXHAUSTED, ::std::ptr::null_mut()); + + Ok(()) + } + + fn enable_object_tagging(&mut self) -> Result<(), ::jvmti::jint> { + let mut capabilities = ::jvmti::jvmtiCapabilities { + _bitfield_1: [0; 4], + _bitfield_2: [0; 2], + _bitfield_3: [0; 2], + _bitfield_4: [0; 2], + __bindgen_align: [], + // FIXME: seems dangeous to reference a field with this name. Same may be true of other fields in this struct. + }; + + jvmtifn!(self.jvmti, GetCapabilities, &mut capabilities); + + capabilities.set_can_tag_objects(1); + + jvmtifn!(self.jvmti, AddCapabilities, &capabilities); Ok(()) } + + fn tag_loaded_classes(&self, tagger: &mut Tag) { + let mut class_count = 0; + let mut class_ptr = ::std::ptr::null_mut(); + jvmtifn!(self.jvmti, GetLoadedClasses, &mut class_count, &mut class_ptr); + + while class_count > 0 { + let mut sig_ptr = ::std::ptr::null_mut(); + jvmtifn!(self.jvmti, GetClassSignature, *class_ptr, &mut sig_ptr, ::std::ptr::null_mut()); + unsafe { + let cstr = CStr::from_ptr(sig_ptr); // sig_ptr is deallocated later + let tag = tagger.class_tag(&cstr.to_str().unwrap().to_string()); + jvmtifn!(self.jvmti, SetTag, *class_ptr, tag); + } + jvmtifn!(self.jvmti, Deallocate, sig_ptr as *mut u8); + + class_count -= 1; + unsafe { class_ptr = class_ptr.offset(1); } + } + } + + fn traverse_live_heap(&self, mut closure: F) + where F: FnMut(::jvmti::jlong, ::jvmti::jlong) { + let callbacks = ::jvmti::jvmtiHeapCallbacks { + heap_iteration_callback: None, + heap_reference_callback: Some(heapReferenceCallback), + primitive_field_callback: None, + array_primitive_value_callback: None, + string_primitive_value_callback: None, + reserved5: None, + reserved6: None, + reserved7: None, + reserved8: None, + reserved9: None, + reserved10: None, + reserved11: None, + reserved12: None, + reserved13: None, + reserved14: None, + reserved15: None, + }; + // Pass closure to the callback as a thin pointer pointing to a fat pointer pointing to the closure. + // See: https://stackoverflow.com/questions/38995701/how-do-i-pass-closures-through-raw-pointers-as-arguments-to-c-functions + let mut closure_ptr: &mut FnMut(::jvmti::jlong, ::jvmti::jlong) = &mut closure; + let closure_ptr_ptr = unsafe { transmute(&mut closure_ptr) }; + + // Need to pass the traversal state into FollowReferences and pick it up in the callback, which may be called multiple times + jvmtifn!(self.jvmti, FollowReferences, 0, ::std::ptr::null_mut(), ::std::ptr::null_mut(), &callbacks, closure_ptr_ptr); + // jvmtiHeapCallbacks callbacks = {}; + // callbacks.heap_reference_callback = &heapRefCallback; + // + // jvmtiError err = jvmti -> FollowReferences(0, NULL, NULL, &callbacks, this); + // if (err != JVMTI_ERROR_NONE) { + // std::cerr << "ERROR: FollowReferences failed: " << err << std::endl; + // throw new std::runtime_error("FollowReferences failed"); + // } + } } #[allow(unused_variables)] @@ -198,13 +261,41 @@ pub static mut EVENT_CALLBACKS: EventCallbacks = EventCallbacks { resource_exhausted: None }; +#[allow(unused_variables)] +unsafe extern "C" fn heapReferenceCallback(reference_kind: ::jvmti::jvmtiHeapReferenceKind, + reference_info: *const ::jvmti::jvmtiHeapReferenceInfo, + class_tag: ::jvmti::jlong, + referrer_class_tag: ::jvmti::jlong, + size: ::jvmti::jlong, + tag_ptr: *mut ::jvmti::jlong, + referrer_tag_ptr: *mut ::jvmti::jlong, + length: ::jvmti::jint, + user_data: *mut ::std::os::raw::c_void) + -> ::jvmti::jint { + if *tag_ptr & TAG_VISITED_MASK == TAG_VISITED_MASK { + return 0; + } + + // For each object encountered, tag it so we can avoid visiting it again + // noting that traverse_live_heap is called at most once in the lifetime of a JVM + *tag_ptr |= TAG_VISITED_MASK; + + // Add the object to the heap stats along with its class signature. + let unmaskedClassTag = class_tag & !TAG_VISITED_MASK; + let closure: &mut &mut FnMut(::jvmti::jlong, ::jvmti::jlong) -> ::jvmti::jint = transmute(user_data); + closure(unmaskedClassTag, size); + + ::jvmti::JVMTI_VISIT_OBJECTS as ::jvmti::jint +} + #[derive(Clone, Copy)] pub struct JniEnv { + #[allow(dead_code)] // TODO: revisit this once port is complete jni: *mut ::jvmti::JNIEnv } impl JniEnv { pub fn new(jni_env: *mut ::jvmti::JNIEnv) -> JniEnv { - JniEnv {jni: jni_env} + JniEnv { jni: jni_env } } } diff --git a/src/heap.rs b/src/heap.rs new file mode 100644 index 0000000..ea96acd --- /dev/null +++ b/src/heap.rs @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::collections::HashMap; +use std::cmp::max; +use std::io::Write; + +pub trait Tag { + fn class_tag(&mut self, sig: &String) -> ::jvmti::jlong; + fn class_signature(&self, tag: ::jvmti::jlong) -> Option; +} + +pub struct Tagger { + next_class_tag: ::jvmti::jlong, + sigs: HashMap<::jvmti::jlong, String>, +} + +impl Tagger { + pub fn new() -> Tagger { + Tagger { + next_class_tag: 0, + sigs: HashMap::new(), + } + } +} + +impl Tag for Tagger { + fn class_tag(&mut self, sig: &String) -> ::jvmti::jlong { + self.next_class_tag += 1; + self.sigs.insert(self.next_class_tag, sig.clone()); + self.next_class_tag + } + + fn class_signature(&self, tag: ::jvmti::jlong) -> Option { + self.sigs.get(&tag).cloned() + } +} + +pub trait Record { + fn recordObject(&mut self, class_name: String, object_size: ::jvmti::jlong); +} + +pub trait Print { + fn print(&self, writer: &mut Write); +} + +#[derive(Default)] +struct ObjectStats { + count: usize, + total_size: ::jvmti::jlong, +} + +pub struct Stats { + java_objects: HashMap +} + +impl Stats { + pub fn new() -> Stats { + Stats { + java_objects: HashMap::new(), + } + } +} + +impl Print for Stats{ + fn print(&self, writer: &mut Write) { + let mut results: Vec<(&String, &ObjectStats)> = self.java_objects.iter().collect(); + results.sort_by(|&(_, s1), &(_, s2)| s2.total_size.cmp(&s1.total_size)); + +// results.truncate(20); // TODO: parameterise + + let max_sig_len = results.iter().map(|&(sig, _)| sig.len()) + .fold(10, |max_len, len| max(max_len, len) ); + + writeln!(writer, "| Instance Count | Total Bytes | Class Name{} |", " ".repeat(max_sig_len - 10)).unwrap(); + + for s in results.iter() { + writeln!(writer, "| {:<14} | {:<11} | {}{} |", s.1.count, s.1.total_size, s.0, " ".repeat(max_sig_len - s.0.len())).unwrap(); + } + } +} + +impl Record for Stats { + fn recordObject(&mut self, class_name: String, object_size: ::jvmti::jlong) { + let s = self.java_objects.entry(class_name).or_insert(Default::default()); + s.count += 1; + s.total_size += object_size; + } +} + +#[cfg(test)] +mod tests { + use super::Tag; + + #[test] + fn remembers_tagged_classes() { + let mut t = super::Tagger::new(); + let c = String::from("c"); + let tag_c = t.class_tag(&c); + let d = String::from("d"); + let tag_d = t.class_tag(&d); + + assert_eq!(c, t.class_signature(tag_c).unwrap()); + assert_eq!(d, t.class_signature(tag_d).unwrap()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 9bcfc44..20ff338 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,7 @@ use std::sync::Mutex; #[macro_use] mod macros; mod env; +mod heap; mod jvmti; mod agentcontroller;