/* * Copyright (c) 2004-2008 Apple Inc. All rights reserved. * * @APPLE_APACHE_LICENSE_HEADER_START@ * * 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. * * @APPLE_APACHE_LICENSE_HEADER_END@ */ #include #include #include #include #include #include #include #include #include #include "CFSoftLinking.h" #include "AutoBlockIterator.h" #include "AutoDefs.h" #include "AutoEnvironment.h" #include "AutoMonitor.h" #include "AutoMemoryScanner.h" #include "AutoRootScanner.h" #include "AutoZone.h" #include "auto_zone.h" namespace Auto { //----- constants. const char* kPropertyListXMLHeader = "\n\n"; const char* kPropertyListXMLFooter = "\n\n"; //----- Monitor -----// Monitor *Monitor::_monitor; // current monitor // // monitor // // Get monitor. Start if not running. // Monitor *Monitor::monitor() { if (Environment::_agc_env._enable_monitor && !_monitor) { _monitor = new Monitor(); } return _monitor; } int (*Monitor::_class_list)(void **buffer, int count); ptr_set *Monitor::_class_set; int Monitor::_class_count; void Monitor::set_class_list(int (*class_list)(void **buffer, int count)) { _class_list = class_list; _class_set = ptr_set_new(); _class_count = 0; } // XXX_PCB: lifted from : struct objc_class_header { struct objc_class *isa; struct objc_class *super_class; const char *name; long version; long info; long instance_size; }; bool Monitor::is_object(void *ptr, long size) { if (_class_list) { int count = _class_list(NULL, 0); if (count > _class_count) { void **buffer = (void**) aux_malloc(count * sizeof(void*)); int new_count = _class_list(buffer, count); while (new_count > count) { count = new_count; buffer = (void**) aux_realloc(buffer, count * sizeof(void*)); new_count = _class_list(buffer, count); } _class_count = count; for (int i = 0; i < count; i++) ptr_set_add(_class_set, buffer[i]); aux_free(buffer); } // XXX_PCB shouldn't be hard coding this! objc_class_header *isa = *(objc_class_header**)ptr; return isa && ptr_set_is_member(_class_set, isa) && (size >= isa->instance_size); } return false; } // // Constructor // Monitor::Monitor() : _argc(0) { // prime the timer nano_time(); } // // open_mach_port() // // Opens the mach communication port. // void Monitor::open_mach_port() { // XXX_PCB: this is quite skanky, using CF from here. we expect to be called at the earliest when a GC thread is registered. CFStringRef format = CFStringCreateWithCString(NULL, "com.apple.auto.%d", kCFStringEncodingUTF8); CFStringRef name = CFStringCreateWithFormat(NULL, NULL, format, getpid()); CFRelease(format); CFMessagePortContext context = { 0, this, NULL, NULL, NULL }; _request_port = CFMessagePortCreateLocal(NULL, name, receive_request, &context, NULL); CFRelease(name); CFRunLoopSourceRef source = CFMessagePortCreateRunLoopSource(NULL, _request_port, 0); CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes); CFRelease(source); CFRunLoopRun(); } // // print // // Formatted print to response pipe. // void Monitor::print(const char *fmt, ...) { va_list ap; va_start(ap, fmt); //XXX_JML needs a lock // XXX_PCB not if it's run off the main runloop. char *buffer; int length = vasprintf(&buffer, fmt, ap); if (buffer) { CFDataAppendBytes(_response_buffer, (const UInt8*)buffer, length); free(buffer); } va_end(ap); } // // tokenize_args // // Scan the request generating an array of arguments. // void Monitor::tokenize_args() { char *cursor = _request; for (_argc = 0; *cursor && _argc < max_args; _argc++) { // skip spaces while (*cursor <= ' ' && *cursor != '\0') *cursor++ = '\0'; // exit if no args if (*cursor == '\0') break; // scan over arg characters if (*cursor == '\"') { cursor++; _args[_argc] = cursor; while (*cursor >= ' ') { if (*cursor == '\"') { *cursor++ = '\0'; break; } cursor++; } } else { // record arg position _args[_argc] = cursor; while (*cursor > ' ') cursor++; } } } // // process_request // // Generate a report based on the supplied request. // XXX_JML number of commands is currently small and performance is not an issue // XXX_JML later might want to be table driven. // XXX_JML request's firs arg is object to respond. // void Monitor::process_request() { if (_argc > 0) { // get the command char *command = _args[0]; if (is_equal(command, "blocks")) { send_all_blocks(); return; } if (is_equal(command, "rootblocks")) { send_root_blocks(); return; } if (is_equal(command, "content")) { send_block_content(); return; } if (is_equal(command, "describe")) { send_block_description(); return; } if (is_equal(command, "leaks")) { send_leaks(); return; } if (is_equal(command, "references")) { send_references(); return; } if (is_equal(command, "roots")) { send_roots(); return; } if (is_equal(command, "samples")) { send_zone_samples(); return; } if (is_equal(command, "samplesAll")) { send_process_samples(); return; } if (is_equal(command, "zones")) { send_all_zones(); return; } print("Unknown command %s\n", command); } } CFDataRef Monitor::receive_request(CFMessagePortRef local, SInt32 msgid, CFDataRef data, void *info) { Monitor *monitor = (Monitor *)info; CFIndex length = CFDataGetLength(data); memcpy(monitor->_request, CFDataGetBytePtr(data), length); CFDataRef reply = monitor->_response_buffer = CFDataCreateMutable(kCFAllocatorMallocZone, 0); // sample the stack. monitor->_stack_bottom = (void*) auto_get_sp(); // scan args monitor->tokenize_args(); // prevent the collector from collecting until we've processed the request. Zone *zone = Zone::zone(); if (zone) zone->block_collector(); // process request generating report monitor->process_request(); // unblock the collector. if (zone) zone->unblock_collector(); // return the response. CFDataAppendBytes(monitor->_response_buffer, (const UInt8*)"\0", 1); monitor->_response_buffer = NULL; return reply; } // // send_all_blocks // // Sends all block information to monitor. // struct send_all_blocks_visitor { Monitor *_monitor; // Constructor send_all_blocks_visitor(Monitor *monitor) : _monitor(monitor) {} // visitor function for subzone inline bool visit(Zone *zone, Subzone *subzone, usword_t q) { // send single block information _monitor->send_block(zone, subzone, q, subzone->quantum_address(q)); // always continue return true; } // visitor function for large inline bool visit(Zone *zone, Large *large) { // send single block information _monitor->send_block(zone, large, large->address()); // always continue return true; } }; static void malloc_block_recorder(task_t task, void *context, unsigned type, vm_range_t *range, unsigned count) { Monitor *monitor = reinterpret_cast(context); for (unsigned i = 0; i < count; i++, range++) { monitor->print("block"); monitor->send_malloc_block_info((void*)range->address, range->size); monitor->print("\n"); } } void Monitor::send_all_blocks() { malloc_zone_t *zone = (malloc_zone_t *)strtoul(_args[2], NULL, 0); print("blocks %s\n", _args[1]); if (zone == (malloc_zone_t *)Zone::zone()) { send_all_blocks_visitor visitor(this); visitAllocatedBlocks(reinterpret_cast(zone), visitor); } else { // use the malloc_zone API for enumerating blocks. zone->introspect->enumerator(mach_task_self(), this, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t) zone, NULL, malloc_block_recorder); } print("\\blocks\n"); } // // send_block_info // // Send specific information about block // void Monitor::send_block_info(Zone *zone, void *block) { if (zone->in_subzone_memory(block)) { Subzone *subzone = Subzone::subzone(block); return send_block_info(zone, subzone, subzone->quantum_index(block), block); } else if (zone->in_large_memory(block)) { return send_block_info(zone, Large::large(block), block); } else { ASSERTION(0 && "not a block"); } } void Monitor::send_block_info(Zone *zone, Subzone *subzone, usword_t q, void *block) { // print block info int rc = zone->block_refcount(block); int layout = subzone->layout(q); bool is_unscanned = (layout & AUTO_UNSCANNED) != 0; bool is_object = (layout & AUTO_OBJECT) != 0; bool is_new = subzone->is_new(q); bool is_marked = subzone->is_marked(q); char *class_name = NULL; if (is_object) { class_name = zone->control.name_for_address((auto_zone_t *)zone, (vm_address_t)block, 0); } print(" %p %lu %d %s%s%s%s %s", block, (unsigned long)subzone->size(q), rc, is_unscanned ? "u" : "s", is_object ? "o" : "m", is_new ? "n" : "o", is_marked ? "m" : "u", class_name ? class_name : ""); if (class_name) free(class_name); } void Monitor::send_block_info(Zone *zone, Large *large, void *block) { // print block info int rc = zone->block_refcount(block); int layout = large->layout(); bool is_unscanned = (layout & AUTO_UNSCANNED) != 0; bool is_object = (layout & AUTO_OBJECT) != 0; bool is_new = large->is_new(); bool is_marked = large->is_marked(); char *class_name = NULL; if (is_object) { class_name = zone->control.name_for_address((auto_zone_t *)zone, (vm_address_t)block, 0); } print(" %p %lu %d %s%s%s%s %s", block, (unsigned long)large->size(), rc, is_unscanned ? "u" : "s", is_object ? "o" : "m", is_new ? "n" : "o", is_marked ? "m" : "u", class_name ? class_name : ""); if (class_name) free(class_name); } // // send_malloc_block_info // // Send fake information about a malloc block. // void Monitor::send_malloc_block_info(void *block, size_t size) { int rc = 1; int layout = AUTO_MEMORY_UNSCANNED; bool is_unscanned = (layout & AUTO_UNSCANNED) != 0; bool is_new = false; bool is_marked = false; const char *class_name = ""; bool is_object = Monitor::is_object(block, size); if (is_object) class_name = (*(objc_class_header**)block)->name; print(" %p %lu %d %s%s%s%s %s", block, size, rc, is_unscanned ? "u" : "s", is_object ? "o" : "m", is_new ? "n" : "o", is_marked ? "m" : "u", class_name); } // // send_block_content // // Send the content of a block to monitor. // void Monitor::send_block_content() { // get zone malloc_zone_t *zone = (malloc_zone_t *)strtoul(_args[2], NULL, 0); // get block void *block = (void *)strtoul(_args[3], NULL, 0); print("content %s\n", _args[1]); if (zone == (malloc_zone_t *)Zone::zone()) { Zone *azone = (Zone *)zone; if (azone->is_block(block)) { usword_t size = azone->block_size(block); for (usword_t offset = 0; offset < size; offset += sizeof(void *)) { intptr_t *slot = (intptr_t *)displace(block, offset); intptr_t content = *slot; print("slot %p %lu %p", slot, offset, content); if (azone->is_block((void *)content)) { send_block_info(azone, (void *)content); } else { // room for comment } print("\n"); } } } else { // it's a malloc_zone_t block of some kind. size_t size = malloc_size(block); if (size != 0) { for (usword_t offset = 0; offset < size; offset += sizeof(void *)) { intptr_t *slot = (intptr_t *)displace(block, offset); intptr_t content = *slot; print("slot %p %lu %p", slot, offset, content); size_t content_size = malloc_size((void *)content); if (content_size) { send_malloc_block_info((void *)content, content_size); } else { // room for comment } print("\n"); } } } print("\\content\n"); } // // send_block_description // // Send a low-level description of the block, if it's owned by our zone, and known to be // an object. If so, then we call CFCopyDescription() on the object, escape the resulting // string so it can be represented in an XML-plist, and return it. // void Monitor::send_block_description() { print(kPropertyListXMLHeader); // get zone malloc_zone_t *zone = (malloc_zone_t *)strtoul(_args[2], NULL, 0); // get block void *block = (void *)strtoul(_args[3], NULL, 0); print("requestor%s\n", _args[1]); print("block%s\n", _args[3]); if (zone == (malloc_zone_t *)Zone::zone()) { Zone *azone = (Zone *)zone; if (azone->is_block(block)) { auto_memory_type_t type = auto_zone_get_layout_type(zone, block); if ((type & AUTO_OBJECT) == AUTO_OBJECT) { // Use CFCopyDescription() rather than sending the object a -description message, because the latter may // crash when containers contain non-object values. CFStringRef description = CFCopyDescription((CFTypeRef)block); if (description) { CFStringRef escaped = CFXMLCreateStringByEscapingEntities(NULL, description, NULL); if (escaped != description) { CFRelease(description); description = escaped; } char buffer[CFStringGetMaximumSizeForEncoding(CFStringGetLength(description), kCFStringEncodingUTF8)]; CFStringGetCString(description, buffer, sizeof(buffer), kCFStringEncodingUTF8); CFRelease(description); print("description%s", buffer); } } } } print(kPropertyListXMLFooter); } // // send_block // // Send details of a block // void Monitor::send_block(Zone *zone, Subzone *subzone, usword_t q, void *block) { print("block"); send_block_info(zone, subzone, q, block); print("\n"); } void Monitor::send_block(Zone *zone, Large *large, void *block) { print("block"); send_block_info(zone, large, block); print("\n"); } // // send_all_zones // // Send addresses of all zones. // void Monitor::send_all_zones() { Zone *zone = Zone::zone(); print("zones %s\n", _args[1]); if (zone) { print("zone %p %p %s\n", zone, zone, malloc_get_zone_name((malloc_zone_t *)zone)); } vm_address_t *zone_addresses; unsigned count = 0; malloc_get_all_zones(mach_task_self(), NULL, &zone_addresses, &count); for (unsigned i = 0; i < count; i++) { malloc_zone_t *malloc_zone = (malloc_zone_t *)zone_addresses[i]; if (malloc_zone != (malloc_zone_t *)zone) print("zone %p 0x00000000 \"%s\"\n", malloc_zone, malloc_get_zone_name(malloc_zone)); } print("\\zones\n"); } // // send_leaks // // Send all blocks that are unreferenced but have a retain count. // struct LeakScanner : public MemoryScanner { LeakScanner(Zone *zone, void *stack_bottom) : MemoryScanner(zone, stack_bottom, false, false) {} virtual void scan_retained_blocks() { // ignored retained blocks for this scan. } }; struct send_leaks_visitor { Monitor *_monitor; // Constructor send_leaks_visitor(Monitor *monitor) : _monitor(monitor) {} // visitor function for subzone inline bool visit(Zone *zone, Subzone *subzone, usword_t q) { // check block if (!subzone->is_marked(q) && subzone->has_refcount(q)) { _monitor->send_block(zone, subzone, q, subzone->quantum_address(q)); } // always continue return true; } // visitor function for large inline bool visit(Zone *zone, Large *large) { // check block if (!large->is_marked() && large->refcount()) { _monitor->send_block(zone, large, large->address()); } // always continue return true; } }; void Monitor::send_leaks() { // get zone Zone *zone = (Zone *)strtoul(_args[2], NULL, 0); // run a FULL collection first. // auto_collect((auto_zone_t *)zone, AUTO_COLLECTION_FULL_COLLECTION, NULL); // scan from threads LeakScanner scanner(zone, _stack_bottom); scanner.scan(); // send response print("leaks %s\n", _args[1]); send_leaks_visitor visitor(this); BlockIterator iterator(zone, visitor); iterator.visit(); print("\\leaks\n"); // clear marks zone->reset_all_marks_and_pending(); } // // send_references // // Send all reference information for the specified block. // struct ReferenceScanner : public MemoryScanner { Monitor *_monitor; // requesting monitor void *_block; // block to find Thread *_thread; // current thread or NULL if not thread int _first_register; // current first register or -1 if not registers Range _thread_range; // current thread range ReferenceScanner(Zone *zone, void *block, Monitor *monitor, void* stack_bottom) : MemoryScanner(zone, stack_bottom, false, true) , _monitor(monitor) , _block(block) , _thread(NULL) , _first_register(-1) , _thread_range() { } virtual void check_block(void **reference, void *block) { MemoryScanner::check_block(reference, block); if (block == _block) { if (_thread) { // use offset BELOW stack bottom. intptr_t offset = (intptr_t)reference - (intptr_t)_thread_range.end(); if (_first_register != -1) { // XXX_JML id needs to be platform specific int id = (offset >> 2) + _first_register; _monitor->print("reference %p %d r %d r%d", reference, offset, id, id); } else { _monitor->print("reference %p %d t %p \"thread stack\"", reference, offset, _thread_range.end()); } } else if (!reference) { _monitor->print("reference 0 0 z 0 \"zone retained\""); } else { void *owner = _zone->block_start((void*)reference); if (owner) { intptr_t offset = (intptr_t)reference - (intptr_t)owner; char *referrer_name = _zone->control.name_for_address((auto_zone_t *)_zone, (vm_address_t)owner, (vm_address_t)offset); _monitor->print("reference %p %d b %p %s", reference, offset, owner, referrer_name); free(referrer_name); _monitor->send_block_info(_zone, (void *)owner); } else if (_zone->is_root(reference)) { Dl_info info; if (dladdr(reference, &info) != 0 && info.dli_saddr == reference) _monitor->print("reference %p 0 b 0 \"global variable: %s\"", reference, info.dli_sname); else _monitor->print("reference %p 0 b 0 \"registered root\"", reference); } else { _monitor->print("reference %p 0 b 0 \"unknown container\"", reference); } } _monitor->print("\n"); } } void scan_range_from_thread(Range &range, Thread *thread) { _thread = thread; _thread_range = range; MemoryScanner::scan_range_from_thread(range, thread); _thread = NULL; _thread_range = Range(); } void scan_range_from_registers(Range &range, Thread *thread, int first_register) { _thread = thread; _first_register = first_register; _thread_range = range; MemoryScanner::scan_range_from_registers(range, thread, first_register); _thread = NULL; _first_register = -1; } }; void Monitor::send_references() { // get zone Zone *zone = (Zone *)strtoul(_args[2], NULL, 0); // get block void *block = (void *)strtoul(_args[3], NULL, 0); // scan from roots ReferenceScanner scanner(zone, block, this, _stack_bottom); // send response print("references %s\n", _args[1]); // scan for references scanner.scan(); // clear marks zone->reset_all_marks_and_pending(); print("\\references\n"); } // // send_roots // // Send all root information for the specified block. // struct MonitorRootScanner : public RootScanner { Monitor *_monitor; // requesting monitor MonitorRootScanner(Zone *zone, void *block, Monitor *monitor, void* stack_bottom) : RootScanner(zone, block, stack_bottom), _monitor(monitor) { } void print_root(ReferenceNode *node, ReferenceNode *nextNode) { void *address = node->address(); switch (node->_kind) { case ReferenceNode::HEAP: usword_t offset = node->offsetOf(nextNode); char *referrer_name = _zone->control.name_for_address((auto_zone_t *)_zone, (vm_address_t)address, offset); _monitor->print("reference %p %u b %p %s", (uintptr_t)address + offset, offset, address, referrer_name); free(referrer_name); _monitor->send_block_info(_zone, address); break; case ReferenceNode::ROOT: Dl_info info; if (dladdr(address, &info) != 0 && info.dli_saddr == address) _monitor->print("reference %p 0 b 0 \"global variable: %s\"", address, info.dli_sname); else _monitor->print("reference %p 0 b 0 \"registered root\"", address); break; case ReferenceNode::STACK: _monitor->print("reference %p %ld t %p \"thread stack\"", address, -(intptr_t)node->size(), node->end()); break; } _monitor->print("\n"); } void print_roots(void *block) { // print the roots of the reference graph. this means go through all the nodes, identifying the roots, which are either blocks that // have a retain count > 0, or registered roots. From one of these roots, perform a bread-first search. usword_t count = _graph._nodes.length(); for (usword_t i = 0; i < count; ++i) { ReferenceNode& node = _graph._nodes[i]; void *address = node.address(); if (node._kind == ReferenceNode::STACK || node._kind == ReferenceNode::ROOT || (_zone->is_block(address) && _zone->block_refcount(address) > 0)) { List path; // if (node._kind == ReferenceNode::STACK) __builtin_trap(); if (_graph.findPath(node.address(), block, path)) { // print out the path. usword_t length = path.length(); for (usword_t j = 1; j <= length; ++j) { ReferenceNode* currentNode = path[length - j]; ReferenceNode* nextNode = (j < length ? path[length - j - 1] : NULL); print_root(currentNode, nextNode); } // generate a separator line. _monitor->print("\n"); } _graph.resetNodes(); } } } }; void Monitor::send_roots() { // get zone Zone *zone = (Zone *)strtoul(_args[2], NULL, 0); // get block void *block = (void *)strtoul(_args[3], NULL, 0); // scan from roots MonitorRootScanner scanner(zone, block, this, _stack_bottom); // try to use the scanning stack for speed. zone->clear_use_pending(); ScanStack &scan_stack = zone->scan_stack(); do { // scan for references scanner.scan(); // clear marks zone->reset_all_marks(); } while (scanner.has_pending_blocks()); zone->set_use_pending(); bool stack_overflow = scan_stack.is_overflow(); scan_stack.reset(); // send response print("roots %s\n", _args[1]); if (!stack_overflow) scanner.print_roots(block); print("\\roots\n"); } // eventually, use a MemoryScanner sub-class (see RootScanner above for an example). We'll need this, to be able // to compute each root's sub-graph size. struct RootFinder : private MemoryScanner { Monitor *_monitor; // requesting monitor bool _scanning_roots; RangeList _list; RootFinder(Zone *zone, Monitor *monitor) : MemoryScanner(zone, NULL, false, true), _monitor(monitor) { } void find() { _scanning_roots = true; scan_root_ranges(); // this scans the roots eagerly. _scanning_roots = false; scan_retained_blocks(); // this pends the retained blocks for scanning. scan_pending_blocks(); // this should call check_block() on all pending blocks. } virtual void check_block(void **reference, void *block) { if (_scanning_roots || _zone->block_refcount(block) != 0) { // only consider scanned blocks. auto_memory_type_t type = (auto_memory_type_t) _zone->block_layout(block); if ((type & AUTO_UNSCANNED) != AUTO_UNSCANNED) { _list.add(Range(block, _zone->block_size(block))); } } } }; struct BlockScanner : public MemoryScanner { Range _blockRange; usword_t _bytesReachable; usword_t _objectsReachable; BlockScanner(Zone *zone, Range block) : MemoryScanner(zone, NULL, false, true), _blockRange(block), _bytesReachable(block.size()), _objectsReachable(0) { } void scan() { scan_range(_blockRange); scan_pending_until_done(); } virtual void check_block(void **reference, void *block) { _bytesReachable += _zone->block_size(block), ++_objectsReachable; MemoryScanner::check_block(reference, block); // to get recursive scanning. } }; void Monitor::send_root_blocks() { Zone *zone = Zone::zone(); zone->set_use_pending(); RootFinder roots(zone, this); roots.find(); print(kPropertyListXMLHeader); print("requestor%s\n", _args[1]); print("rootBlocks\n"); for (usword_t i = 0; i < roots._list.length(); ++i) { BlockScanner scanner(zone, roots._list[i]); scanner.scan(); _monitor->print("%p%lu%lu\n", scanner._blockRange.address(), scanner._bytesReachable, scanner._objectsReachable); } zone->reset_all_marks_and_pending(); print("\n"); print(kPropertyListXMLFooter); } // // send_zone_samples // // Send sample of statistics for the specified zone. // void Monitor::send_zone_samples() { malloc_zone_t *malloc_zone = (malloc_zone_t *)strtoul(_args[2], NULL, 0); malloc_statistics_t stats; malloc_zone_statistics(malloc_zone, &stats); print("samples %s\n", _args[1]); print("sample %f %u %zu %zu %zu\n", nano_time(), stats.blocks_in_use, stats.size_in_use, stats.max_size_in_use, stats.size_allocated); print("\\samples\n"); } // // send_process_samples // // Send sample of statistics for the entire process. // void Monitor::send_process_samples() { vm_address_t *zone_addresses; unsigned count = 0; malloc_get_all_zones(mach_task_self(), NULL, &zone_addresses, &count); malloc_statistics_t stats; bzero(&stats, sizeof(malloc_statistics_t)); for (unsigned i = 0; i < count; i++) { malloc_zone_t *malloc_zone = (malloc_zone_t *)zone_addresses[i]; malloc_statistics_t zone_stats; malloc_zone_statistics(malloc_zone, &zone_stats); stats.blocks_in_use += zone_stats.blocks_in_use; stats.size_in_use += zone_stats.size_in_use; stats.max_size_in_use += zone_stats.max_size_in_use; stats.size_allocated += zone_stats.size_allocated;; } print("samples %s\n", _args[1]); print("sample %f %u %zu %zu %zu\n", nano_time(), stats.blocks_in_use, stats.size_in_use, stats.max_size_in_use, stats.size_allocated); print("\\samples\n"); } };