// // This code communicates the collected information about memory allocations // to an external visualization client like examples/codex_view. // vis_init :: () { // We allow you to start the visualization client after the program is running, so // if we have not connected, we keep trying. MILLISECONDS_PER_VIS_INIT controls // how often we retry. now := current_time_monotonic(); if last_update_time != APOLLO_TIME_INVALID { ms := to_milliseconds(now - last_update_time); if ms < MILLISECONDS_PER_VIS_INIT return; } last_update_time = now; // It's the first try, or it's been long enough since the last try. // So, let's try to connect. success, channel := writer_connect(MEMORY_VISUALIZATION_CHANNEL_NAME, name_not_found_is_quiet=true); if !success { if !logged_retry { log("Unable to connect to a memory visualization client. Will retry in case one is started.\n"); logged_retry = true; } return; } vis_initted = true; bytes_total := size_of(Memory_Debugger_Version_Info); message_success, message := writer_reserve_message(*channel, bytes_total); if !message_success { log_error("In vis_init(), we were unable to reserve the initial message on the channel. Disabling.\n"); return; } info := cast(*Memory_Debugger_Version_Info) message.user_write_pointer; info.tag = .RESET_AND_TELL_VERSION; info.version = VIS_PROTOCOL_VERSION; info.default_allocator_proc = runtime_support_default_allocator_proc; writer_commit_message(*channel, message); vis_channel = channel; vis_enabled = true; } maybe_add_string :: (s: string) { found, value := table_find(*table_sent_strings, s); if !found { array_add(*to_send_strings, s); index := cast(s32) table_sent_strings.count; table_set(*table_sent_strings, s, index); } } vis_update_if_it_has_been_long_enough :: () { if !vis_initted vis_init(); if !vis_enabled return; if did_user_level_per_frame_update return; // Once the user calls a manual update, we expect that is the way it should go henceforth. now := current_time_monotonic(); if last_update_time == APOLLO_TIME_INVALID { last_update_time = now; } else { delta := now - last_update_time; ms := to_milliseconds(delta); if ms >= MILLISECONDS_PER_VIS_UPDATE { vis_update(); last_update_time = now; } } } vis_update :: () { // // :InsideMemoryDebugger // // We set inside_memory_debugger so that we don't mess up the accounting that the user sees, // and also, so that the report size stays stable while we are trying to transmit it. // context.inside_memory_debugger = true; defer context.inside_memory_debugger = false; // For each pending stack trace, see if we need to add its strings // and Procedure_Infos. for trace: pending_stack_traces { for node: trace.nodes { info := node.info; found, value := table_find(*table_sent_procedure_infos, info); if !found { array_add(*to_send_procedure_infos, info); table_set(*table_sent_procedure_infos, info, true); maybe_add_string(info.name); maybe_add_string(info.location.fully_pathed_filename); } } array_add(*to_send_stack_traces, trace); } pending_stack_traces.count = 0; // Send data for the new strings. for s: to_send_strings { count := cast,trunc(s32) s.count; if count < 0 count = 0; bytes_total := size_of(String_Info) + count; success, message := writer_reserve_message(*vis_channel, bytes_total); if !success break; // We need to wait until later...! data := message.user_write_pointer; info := cast(*String_Info) data; data += size_of(String_Info); found, index := table_find(*table_sent_strings, s); assert(found); info.tag = .ADD_STRING; info.index = cast(String_Index) index; info.count = count; memcpy(data, s.data, count); writer_commit_message(*vis_channel, message); remove s; } // Send data for the new Procedure_Infos. for info: to_send_procedure_infos { bytes_total := size_of(Packed_Stack_Trace_Procedure_Info); success, message := writer_reserve_message(*vis_channel, bytes_total); if !success break; // We need to wait until later...! p := cast(*Packed_Stack_Trace_Procedure_Info) message.user_write_pointer; name_found, name_index := table_find(*table_sent_strings, info.name); assert(name_found); filename_found, filename_index := table_find(*table_sent_strings, info.location.fully_pathed_filename); assert(filename_found); p.tag = .ADD_PROCEDURE_INFO; p.name = cast(String_Index) name_index; p.filename = cast(String_Index) filename_index; p.line_number = cast,trunc(s32) info.location.line_number; p.character_number = cast,trunc(s32) info.location.character_number; p.remote_address = info; writer_commit_message(*vis_channel, message); remove info; } // Now send data for the new stack traces. for trace: to_send_stack_traces { // @Incomplete: Names of these Stack_Trace_Nodes are in the wrong address space, // so we can't draw them yet. We will probably send mappings for the strings // so that the visualizer can put them in a hash table. bytes := size_of(Single_Stack_Trace_Info) + trace.nodes.count * size_of(Stack_Trace_Node); success, message := writer_reserve_message(*vis_channel, bytes); if !success break; // We need to wait until later...! // Write the ADD_STACK_TRACE command, the hash, and the address in our address space // of this Packed_Stack_Trace (since the Leak_Summary uses that). data := message.user_write_pointer; info := cast(*Single_Stack_Trace_Info) data; info.tag = .ADD_STACK_TRACE; info.hash = trace.hash; info.address = trace; info.num_nodes = cast(s32) trace.nodes.count; data += size_of(Single_Stack_Trace_Info); memcpy(data, trace.nodes.data, trace.nodes.count * size_of(Stack_Trace_Node)); writer_commit_message(*vis_channel, message); remove trace; } // Dump the current report. // @Speed -- now the amount of time it takes to generate the report matters, // since we are doing it during runtime, so we will want to report := make_leak_report(for_visualizer=true); // @Incomplete: No way to set leak_report_options from here yet. defer deinit(*report); // // :InsideMemoryDebugger // // make_leak_report sets this to false, assuming that the caller // is not inside the memory debugger. Mayhap we should do this in a more-formal way, // but for now we just set it back to true. (Keep in mind there is a 'defer' active // that will set it to false, still, when we close the scope.) // context.inside_memory_debugger = true; // // When accessing allocs_this_frame and frees_this_frame, we must be holding address_table_mutex, // since these could be modified by alloc(), free(), realloc() etc, which can be called on many threads. // We would prefer not to be doing much work inside this mutex, *but* report.sorted_summaries could be // quite large, so we copy the per-frame data (which is hopefully pretty small) so that we don't have to // hold the mutex that whole time. There could be some other creative solutions to the problem; for example, // we only need to know the byte total because we are sending a single message containing all the data, // but if we broke the mutex-controlled data into a separate message, we could perhaps avoid some of // this entanglement (but then you'd have to be guaranteed that the message target memory allocation is fast...) // bytes_total: int; bytes_for_mutex_controlled_data: int; num_allocs_this_frame: s32; num_frees\_this_frame: s32; { lock(*address_table_mutex); defer unlock(*address_table_mutex); bytes_for_mutex_controlled_data := allocs_this_frame.count * size_of(Allocs_This_Frame) + frees_this_frame.count * size_of(Frees_This_Frame); bytes_total = size_of(Summary_Info) + report.sorted_summaries.count * size_of(Packed_Summary) + bytes_for_mutex_controlled_data; num_allocs_this_frame = cast(s32) allocs_this_frame.count; num_frees\_this_frame = cast(s32) frees\_this_frame.count; if mutex_controlled_data_copy.allocated < bytes_for_mutex_controlled_data { // We have an array that just sits there and grows over time, // never recycling the memory, so hopefully we do not do this // array_reserve very often! // :InsideMemoryDebugger // // This is one of the reasons we want inside_memory_debugger = true; this allocation // would otherwise show up on stats. push_allocator(Context_Base.default_allocator); array_reserve(*mutex_controlled_data_copy, bytes_for_mutex_controlled_data); } mutex_controlled_data_copy.count = bytes_for_mutex_controlled_data; data := mutex_controlled_data_copy.data; for * allocs_this_frame { p := cast(*Allocs_This_Frame) data; p.* = it.*; data += size_of(Allocs_This_Frame); } for * frees_this_frame { p := cast(*Frees_This_Frame) data; p.* = it.*; data += size_of(Frees_This_Frame); } assert(data == mutex_controlled_data_copy.data + mutex_controlled_data_copy.count); } success, message := writer_reserve_message(*vis_channel, bytes_total); if success { data := message.user_write_pointer; info := cast(*Summary_Info) data; info.tag = .UPDATE_USAGE; info.num_live = cast(s32) report.sorted_summaries.count; info.num_allocs_this_frame = num_allocs_this_frame; info.num_frees\_this_frame = num_frees_this_frame; data += size_of(Summary_Info); for report.sorted_summaries { p := cast(*Packed_Summary) data; p.count = it.count; p.bytes = it.bytes; p.allocations\ _since_last_visualizer_update = it.allocations\ _since_last_visualizer_update; p.bytes_allocated_since_last_visualizer_update = it.bytes_allocated_since_last_visualizer_update; p.alloc_site_trace = it.alloc_site_trace; p.allocator_proc = it.allocator_proc; if it.group_info { p.grouped_from_this_many_stack_traces = cast(s32) it.group_info.sub_summaries.count; p.root_nodes_in_common = cast(s32) it.group_info.root_nodes_in_common; p.leaf_nodes_in_common = cast(s32) it.group_info.leaf_nodes_in_common; } else { p.grouped_from_this_many_stack_traces = 0; p.root_nodes_in_common = 0; p.leaf_nodes_in_common = 0; } data += size_of(Packed_Summary); } memcpy(data, mutex_controlled_data_copy.data, mutex_controlled_data_copy.count); data += mutex_controlled_data_copy.count; assert(data == message.user_write_pointer + bytes_total); writer_commit_message(*vis_channel, message); } { lock(*address_table_mutex); defer unlock(*address_table_mutex); table_reset(*allocs_this_frame); // Leave the memory of these arrays active so we don't keep allocating it. table_reset(*frees_this_frame); } } vis_fini :: () { if !vis_enabled return; writer_disconnect(*vis_channel); vis_enabled = false; } memory_visualizer_per_frame_update :: () { did_user_level_per_frame_update = true; // @Threadsafety... if we turn back on the long_enough thing, we have a race here. if !vis_initted vis_init(); if !vis_enabled return; // This is a user-level routine that you call in order to output memory info // at a controlled time. vis_update(); } #scope_module pending_stack_traces: [..] *Packed_Stack_Trace; to_send_stack_traces: [..] *Packed_Stack_Trace; // When we get a new stack trace, we pool the data items we need to send // into these arrays, then bleed them off as we send messages. to_send_strings: [..] string; to_send_procedure_infos: [..] *Stack_Trace_Procedure_Info; // The pending_stack_traces are only sent to us when we add a new one // to a table from the core Memory_Debugger stuff. But there is no such // tracking for strings or Procedure_Infos, so we do that ourselves, // so we only need to send these once to the viewer. table_sent_strings: Table(string, s32); table_sent_procedure_infos: Table(*Stack_Trace_Procedure_Info, bool); mutex_controlled_data_copy: [..] u8; #scope_file vis_channel: Write_Channel; vis_enabled := false; logged_retry := false; vis_initted := false; // Did we try to call vis_init() yet? did_user_level_per_frame_update := false; last_update_time := APOLLO_TIME_INVALID; last_vis_init_time := APOLLO_TIME_INVALID; MILLISECONDS_PER_VIS_UPDATE :: 250; MILLISECONDS_PER_VIS_INIT :: 500; #import "Shared_Memory_Channel"; #import "Protocol_For_Memory_Visualization"; #import "Hash_Table";