370 lines
14 KiB
Plaintext
370 lines
14 KiB
Plaintext
//
|
|
// 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";
|