| /* |
| * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. |
| * Copyright (c) 2023, 2025, Red Hat, Inc. and/or its affiliates. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| * |
| */ |
| |
| #include "code/nmethod.hpp" |
| #include "compiler/abstractCompiler.hpp" |
| #include "compiler/compilationMemStatInternals.inline.hpp" |
| #include "compiler/compilerDefinitions.inline.hpp" |
| #include "compiler/compilerDirectives.hpp" |
| #include "compiler/compilerOracle.hpp" |
| #include "compiler/compilerThread.hpp" |
| #include "compiler/compileTask.hpp" |
| #include "logging/log.hpp" |
| #include "logging/logStream.hpp" |
| #include "memory/arena.hpp" |
| #include "nmt/nmtCommon.hpp" |
| #include "oops/method.inline.hpp" |
| #include "oops/symbol.hpp" |
| #include "runtime/atomic.hpp" |
| #include "runtime/mutexLocker.hpp" |
| #include "runtime/os.hpp" |
| #include "utilities/checkedCast.hpp" |
| #include "utilities/debug.hpp" |
| #include "utilities/globalDefinitions.hpp" |
| #include "utilities/ostream.hpp" |
| |
| #ifdef COMPILER1 |
| #include "c1/c1_Compilation.hpp" |
| #endif |
| |
| #ifdef COMPILER2 |
| #include "opto/compile.hpp" |
| #include "opto/node.hpp" // compile.hpp is not self-contained |
| #endif |
| |
| static const char* phase_trc_id_to_string(int phase_trc_id) { |
| return COMPILER2_PRESENT(Phase::get_phase_trace_id_text((Phase::PhaseTraceId)phase_trc_id)) |
| NOT_COMPILER2(""); |
| } |
| |
| // If crash option on memlimit is enabled and an oom occurred, the stats for the |
| // first offending compilation. |
| static ArenaStatCounter* volatile _arenastat_oom_crash = nullptr; |
| |
| // Arena-chunk stamping |
| union chunkstamp_t { |
| uint64_t raw; |
| struct { |
| uint32_t tracked; |
| uint16_t arena_tag; |
| uint16_t phase_id; |
| }; |
| }; |
| STATIC_ASSERT(sizeof(chunkstamp_t) == sizeof(chunkstamp_t::raw)); |
| |
| ArenaCounterTable::ArenaCounterTable() { |
| memset(_v, 0, sizeof(_v)); |
| } |
| |
| void ArenaCounterTable::copy_from(const ArenaCounterTable& other) { |
| memcpy(_v, other._v, sizeof(_v)); |
| } |
| |
| void ArenaCounterTable::summarize(size_t out[arena_tag_max]) const { |
| memset(out, 0, arena_tag_max * sizeof(size_t)); |
| for (int i = 0; i < phase_trc_id_max; i++) { |
| for (int j = 0; j < arena_tag_max; j++) { |
| out[j] += _v[i][j]; |
| } |
| } |
| } |
| |
| void ArenaCounterTable::print_on(outputStream* st) const { |
| bool header_printed = false; |
| for (int phase_trc_id = 0; phase_trc_id < phase_trc_id_max; phase_trc_id++) { |
| size_t sum = 0; |
| for (int arena_tag = 0; arena_tag < arena_tag_max; arena_tag++) { |
| sum += at(phase_trc_id, arena_tag); |
| } |
| if (sum > 0) { // omit phases that did not contribute to allocation load |
| if (!header_printed) { |
| st->print("%-24s %10s", "Phase", "Total"); |
| for (int arena_tag = 0; arena_tag < arena_tag_max; arena_tag++) { |
| st->print("%10s", Arena::tag_name[arena_tag]); |
| } |
| st->cr(); |
| header_printed = true; |
| } |
| st->print("%-24s ", phase_trc_id_to_string(phase_trc_id)); |
| st->print("%10zu", sum); |
| for (int arena_tag = 0; arena_tag < arena_tag_max; arena_tag++) { |
| const size_t v = at(phase_trc_id, arena_tag); |
| st->print("%10zu", v); |
| } |
| st->cr(); |
| } |
| } |
| } |
| |
| // When reporting phase footprint movements, if phase-local peak over start as well over end |
| // was larger than this threshold, we report it. |
| static constexpr size_t significant_peak_threshold = M; |
| |
| FootprintTimeline::FootprintTimeline() { |
| DEBUG_ONLY(_inbetween_phases = true;) |
| } |
| |
| void FootprintTimeline::copy_from(const FootprintTimeline& other) { |
| _fifo.copy_from(other._fifo); |
| DEBUG_ONLY(_inbetween_phases = other._inbetween_phases;) |
| } |
| |
| void FootprintTimeline::print_on(outputStream* st) const { |
| const int start_indent = st->indentation(); |
| if (!_fifo.empty()) { |
| // .123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789 |
| st->print_cr("Phase seq. number Bytes Nodes"); |
| unsigned from = 0; |
| if (_fifo.lost() > 0) { |
| st->print_cr(" (" UINT64_FORMAT " older entries lost)", _fifo.lost()); |
| } |
| int last_level = 0; |
| int last_num = 0; // see if we are regressing |
| auto printer = [&](const Entry& e) { |
| int col = start_indent; |
| check_phase_trace_id(e.info.id); |
| st->print("%*s", e.level, |
| ((e.level < last_level) ? "<" : ((e.level > last_level) ? ">" : " ")) |
| ); |
| last_level = e.level; |
| st->print("%d ", e.info.num); |
| if (e.info.num < last_num) { |
| st->print("(cont.) "); |
| } |
| last_num = e.info.num; |
| col += 15; st->fill_to(col); |
| st->print("%24s", e.info.text); |
| col += 25; st->fill_to(col); |
| char tmp[64]; |
| os::snprintf(tmp, sizeof(tmp), "%9zu (%+zd)", e._bytes.cur, e._bytes.end_delta()); |
| st->print("%s ", tmp); // end |
| col += 21; st->fill_to(col); |
| os::snprintf(tmp, sizeof(tmp), "%6u (%+d)", e._live_nodes.cur, e._live_nodes.end_delta()); |
| st->print("%s ", tmp); // end |
| if (e._bytes.temporary_peak_size() > significant_peak_threshold) { |
| col += 20; st->fill_to(col); |
| st->print(" significant temporary peak: %zu (%+zd)", e._bytes.peak, (ssize_t)e._bytes.peak - e._bytes.start); // peak |
| } |
| st->cr(); |
| }; |
| _fifo.iterate_all(printer); |
| } |
| } |
| |
| void FootprintTimeline::on_phase_end(size_t cur_abs, unsigned cur_nodes) { |
| const Entry& old = _fifo.current(); |
| |
| // One last counter update in old phase: |
| // We see all allocations, so cur_abs given should correspond to our topmost cur. |
| // But the same does not hold for nodes, since we only get updated when node allocation |
| // would cause a new arena chunk to be born. Node allocations that don't cause arena |
| // chunks (the vast majority) fly by us. |
| assert(old._bytes.cur == cur_abs, "miscount"); |
| on_footprint_change(cur_abs, cur_nodes); |
| |
| // Close old, open new entry |
| _fifo.advance(); |
| |
| DEBUG_ONLY(_inbetween_phases = true;) |
| } |
| |
| void FootprintTimeline::on_phase_start(PhaseInfo info, size_t cur_abs, unsigned cur_nodes, int level) { |
| if (!_fifo.empty() && _fifo.last().info.id == info.id && _fifo.last().level == level) { |
| // Two phases with the same id are collapsed if they were not interleaved by another phase |
| _fifo.revert(); |
| // We now just continue bookkeeping into the last entry |
| } else { |
| // seed current entry |
| Entry& e = _fifo.current(); |
| e._bytes.init(cur_abs); |
| e._live_nodes.init(cur_nodes); |
| e.info = info; |
| e.level = level; |
| } |
| DEBUG_ONLY(_inbetween_phases = false;) |
| } |
| |
| FullMethodName::FullMethodName() : _k(nullptr), _m(nullptr), _s(nullptr) {} |
| |
| FullMethodName::FullMethodName(const Method* m) : |
| _k(m->klass_name()), _m(m->name()), _s(m->signature()) {}; |
| |
| FullMethodName::FullMethodName(const FullMethodName& o) : _k(o._k), _m(o._m), _s(o._s) {} |
| |
| FullMethodName& FullMethodName::operator=(const FullMethodName& o) { |
| _k = o._k; _m = o._m; _s = o._s; |
| return *this; |
| } |
| |
| void FullMethodName::make_permanent() { |
| _k->make_permanent(); |
| _m->make_permanent(); |
| _s->make_permanent(); |
| } |
| |
| void FullMethodName::print_on(outputStream* st) const { |
| char tmp[1024]; |
| st->print_raw(_k->as_C_string(tmp, sizeof(tmp))); |
| st->print_raw("::"); |
| st->print_raw(_m->as_C_string(tmp, sizeof(tmp))); |
| st->put('('); |
| st->print_raw(_s->as_C_string(tmp, sizeof(tmp))); |
| st->put(')'); |
| } |
| |
| char* FullMethodName::as_C_string(char* buf, size_t len) const { |
| stringStream ss(buf, len); |
| print_on(&ss); |
| return buf; |
| } |
| |
| bool FullMethodName::operator== (const FullMethodName& b) const { |
| return _k == b._k && _m == b._m && _s == b._s; |
| } |
| |
| #ifdef ASSERT |
| bool FullMethodName::is_test_class() const { |
| char tmp[1024]; |
| _k->as_C_string(tmp, sizeof(tmp)); |
| return strstr(tmp, "CompileCommandPrintMemStat") != nullptr || |
| strstr(tmp, "CompileCommandMemLimit") != nullptr; |
| } |
| #endif // ASSERT |
| |
| ArenaStatCounter::ArenaStatCounter(const CompileTask* task, size_t limit) : |
| _fmn(task->method()), |
| _should_print_memstat(task->directive()->should_print_memstat()), |
| _should_crash_on_memlimit(task->directive()->should_crash_at_mem_limit()), |
| _current(0), _peak(0), _live_nodes_current(0), _live_nodes_at_global_peak(0), |
| _limit(limit), _hit_limit(false), _limit_in_process(false), |
| _phase_counter(0), _comp_type(task->compiler()->type()), _comp_id(task->compile_id()) |
| DEBUG_ONLY(COMMA _is_test_class(false)) |
| { |
| _fmn.make_permanent(); |
| #ifdef ASSERT |
| // If the class name matches the JTreg test class, we are in test mode and |
| // will do some test allocations to test the statistic |
| _is_test_class = _fmn.is_test_class(); |
| #endif // ASSERT |
| } |
| |
| void ArenaStatCounter::on_phase_start(PhaseInfo info) { |
| // Update node counter |
| _live_nodes_current = retrieve_live_node_count(); |
| |
| // For the timeline, when nesting TracePhase happens, we maintain the illusion of a flat succession of |
| // separate phases. Thus, { TracePhase p1; { TracePhase p2; }} will be seen as: |
| // P1 starts -> P1 ends -> P2 starts -> P2 ends -> P1 starts -> P1 ends |
| // In other words, when a child phase interrupts a parent phase, it "ends" the parent phase, which will |
| // be "restarted" when the child phase ends. |
| // This is the only way to get a per-phase timeline that makes any sort of sense. |
| if (!_phase_info_stack.empty()) { |
| _timeline.on_phase_end(_current, _live_nodes_current); |
| } |
| _phase_info_stack.push(info); |
| _timeline.on_phase_start(info, _current, _live_nodes_current, _phase_info_stack.depth()); |
| } |
| |
| void ArenaStatCounter::on_phase_end() { |
| PhaseInfo top = _phase_info_stack.top(); |
| _phase_info_stack.pop(); |
| _live_nodes_current = retrieve_live_node_count(); |
| _timeline.on_phase_end(_current, _live_nodes_current); |
| if (!_phase_info_stack.empty()) { |
| // "restart" parent phase in timeline |
| _timeline.on_phase_start(_phase_info_stack.top(), _current, _live_nodes_current, _phase_info_stack.depth()); |
| } |
| } |
| |
| int ArenaStatCounter::retrieve_live_node_count() const { |
| int result = 0; |
| #ifdef COMPILER2 |
| if (_comp_type == compiler_c2) { |
| // Update C2 node count |
| // Careful, Compile::current() may be null in a short time window when Compile itself |
| // is still being constructed. |
| if (Compile::current() != nullptr) { |
| result = Compile::current()->live_nodes(); |
| } |
| } |
| #endif // COMPILER2 |
| return result; |
| } |
| |
| // Account an arena allocation. Returns true if new peak reached. |
| bool ArenaStatCounter::on_arena_chunk_allocation(size_t size, int arena_tag, uint64_t* stamp) { |
| bool rc = false; |
| |
| const size_t old_current = _current; |
| _current += size; |
| assert(_current >= old_current, "Overflow"); |
| |
| const int phase_trc_id = _phase_info_stack.top().id; |
| _counters_current.add(size, phase_trc_id, arena_tag); |
| _live_nodes_current = retrieve_live_node_count(); |
| |
| _timeline.on_footprint_change(_current, _live_nodes_current); |
| |
| // Did we reach a global peak? |
| if (_current > _peak) { |
| _peak = _current; |
| // snapshot all current counters |
| _counters_at_global_peak.copy_from(_counters_current); |
| // snapshot live nodes |
| _live_nodes_at_global_peak = _live_nodes_current; |
| // Did we hit the memory limit? |
| if (!_hit_limit && _limit > 0 && _peak > _limit) { |
| _hit_limit = true; |
| } |
| // report peak back |
| rc = true; |
| } |
| |
| // calculate arena chunk stamp |
| chunkstamp_t cs; |
| cs.tracked = 1; |
| cs.arena_tag = checked_cast<uint16_t>(arena_tag); |
| cs.phase_id = checked_cast<uint16_t>(_phase_info_stack.top().id); |
| *stamp = cs.raw; |
| |
| return rc; |
| } |
| |
| void ArenaStatCounter::on_arena_chunk_deallocation(size_t size, uint64_t stamp) { |
| assert(_current >= size, "Underflow (%zu %zu)", size, _current); |
| |
| // Extract tag and phase id from stamp |
| chunkstamp_t cs; |
| cs.raw = stamp; |
| assert(cs.tracked == 1, "Sanity"); |
| const int arena_tag = cs.arena_tag; |
| assert(arena_tag >= 0 && arena_tag < arena_tag_max, "Arena Tag OOB (%d)", arena_tag_max); |
| const int phase_trc_id(cs.phase_id); |
| assert(phase_trc_id >= 0 && phase_trc_id < phase_trc_id_max, "Phase trace id OOB (%d)", phase_trc_id); |
| |
| _current -= size; |
| _counters_current.sub(size, phase_trc_id, arena_tag); |
| _live_nodes_current = retrieve_live_node_count(); |
| _timeline.on_footprint_change(_current, _live_nodes_current); |
| } |
| |
| // Used for logging, not for the report table generated with jcmd Compiler.memory |
| void ArenaStatCounter::print_peak_state_on(outputStream* st) const { |
| st->print("Total Usage: %zu ", _peak); |
| if (_peak > 0) { |
| #ifdef COMPILER1 |
| // C1: print allocations broken down by arena types |
| if (_comp_type == CompilerType::compiler_c1) { |
| st->print("["); |
| size_t sums[arena_tag_max]; |
| _counters_at_global_peak.summarize(sums); |
| bool print_comma = false; |
| for (int i = 0; i < arena_tag_max; i++) { |
| if (sums[i] > 0) { |
| if (print_comma) { |
| st->print_raw(", "); |
| } |
| st->print("%s %zu", Arena::tag_name[i], sums[i]); |
| print_comma = true; |
| } |
| } |
| st->print_cr("]"); |
| } |
| #endif // COMPILER1 |
| #ifdef COMPILER2 |
| // C2: print counters and timeline on multiple lines, indented |
| if (_comp_type == CompilerType::compiler_c2) { |
| StreamIndentor si(st, 4); |
| st->cr(); |
| st->print_cr("--- Arena Usage by Arena Type and compilation phase, at arena usage peak of %zu ---", _peak); |
| { |
| StreamIndentor si(st, 4); |
| _counters_at_global_peak.print_on(st); |
| } |
| st->print_cr("--- Allocation timelime by phase ---"); |
| { |
| StreamIndentor si(st, 4); |
| _timeline.print_on(st); |
| } |
| st->print_cr("---"); |
| } |
| #endif |
| } else { |
| st->cr(); |
| } |
| } |
| |
| class MemStatEntry : public CHeapObj<mtCompiler> { |
| |
| FullMethodName _fmn; |
| CompilerType _comp_type; |
| int _comp_id; |
| double _time; |
| // Compiling thread. Only for diagnostic purposes. Thread may not be alive anymore. |
| const Thread* _thread; |
| // active limit for this compilation, if any |
| size_t _limit; |
| // true if the compilation hit the limit |
| bool _hit_limit; |
| // result as reported by compiler |
| const char* _result; |
| |
| // Bytes total at global peak |
| size_t _peak; |
| // Bytes per arena tag. |
| size_t _peak_composition_per_arena_tag[arena_tag_max]; |
| // Number of live nodes at global peak (C2 only) |
| unsigned _live_nodes_at_global_peak; |
| |
| struct Details { |
| ArenaCounterTable counters_at_global_peak; |
| FootprintTimeline timeline; |
| }; |
| |
| Details* _detail_stats; |
| |
| MemStatEntry(const MemStatEntry& e); // deny |
| |
| public: |
| |
| MemStatEntry() |
| : _comp_type(compiler_none), _comp_id(-1), |
| _time(0), _thread(nullptr), _limit(0), _hit_limit(false), |
| _result(nullptr), _peak(0), _live_nodes_at_global_peak(0), |
| _detail_stats(nullptr) { |
| } |
| |
| ~MemStatEntry() { |
| clean_details(); |
| } |
| |
| void set_comp_id(int comp_id) { _comp_id = comp_id; } |
| void set_comptype(CompilerType comptype) { _comp_type = comptype; } |
| void set_current_time() { _time = os::elapsedTime(); } |
| void set_current_thread() { _thread = Thread::current(); } |
| void set_limit(size_t limit) { _limit = limit; } |
| |
| void set_from_state(const ArenaStatCounter* state, bool store_details) { |
| _fmn = state->fmn(); |
| _comp_type = state->comp_type(); |
| _comp_id = state->comp_id(); |
| _limit = state->limit(); |
| _hit_limit = state->hit_limit(); |
| _peak = state->peak(); |
| _live_nodes_at_global_peak = state->live_nodes_at_global_peak(); |
| state->counters_at_global_peak().summarize(_peak_composition_per_arena_tag); |
| #ifdef COMPILER2 |
| assert(_detail_stats == nullptr, "should have been cleaned"); |
| if (store_details) { |
| _detail_stats = NEW_C_HEAP_OBJ(Details, mtCompiler); |
| _detail_stats->counters_at_global_peak.copy_from(state->counters_at_global_peak()); |
| _detail_stats->timeline.copy_from(state->timeline()); |
| } |
| #endif // COMPILER2 |
| } |
| |
| void clean_details() { |
| if (_detail_stats != nullptr) { |
| FREE_C_HEAP_ARRAY(Details, _detail_stats); |
| _detail_stats = nullptr; |
| } |
| } |
| |
| void reset() { |
| clean_details(); |
| _comp_type = CompilerType::compiler_none; |
| _comp_id = -1; |
| _limit = _peak = 0; |
| _live_nodes_at_global_peak = 0; |
| memset(_peak_composition_per_arena_tag, 0, sizeof(_peak_composition_per_arena_tag)); |
| } |
| |
| void set_result(const char* s) { _result = s; } |
| |
| size_t peak() const { return _peak; } |
| bool is_c1() const { return _comp_type == CompilerType::compiler_c1; } |
| bool is_c2() const { return _comp_type == CompilerType::compiler_c2; } |
| |
| static void print_legend(outputStream* st) { |
| #define LEGEND_KEY_FMT "%11s" |
| st->print_cr("Legend:"); |
| st->print_cr(" " LEGEND_KEY_FMT ": %s", "ctype", "compiler type"); |
| st->print_cr(" " LEGEND_KEY_FMT ": %s", "total", "peak memory allocated via arenas while compiling"); |
| for (int tag = 0; tag < arena_tag_max; tag++) { |
| st->print_cr(" " LEGEND_KEY_FMT ": %s", Arena::tag_name[tag], Arena::tag_desc[tag]); |
| } |
| st->print_cr(" " LEGEND_KEY_FMT ": %s", "#nodes", "...how many nodes (c2 only)"); |
| st->print_cr(" " LEGEND_KEY_FMT ": %s", "result", "Result reported by compiler"); |
| st->print_cr(" " LEGEND_KEY_FMT ": %s", "limit", "memory limit; followed by \"*\" if the limit was hit"); |
| st->print_cr(" " LEGEND_KEY_FMT ": %s", "time", "timestamp"); |
| st->print_cr(" " LEGEND_KEY_FMT ": %s", "id", "compile id"); |
| st->print_cr(" " LEGEND_KEY_FMT ": %s", "thread", "compiler thread"); |
| #undef LEGEND_KEY_FMT |
| } |
| |
| static void print_header(outputStream* st) { |
| st->print("%-6s", "ctyp"); |
| |
| #define SIZE_FMT "%-10s" |
| st->print(SIZE_FMT, "total"); |
| for (int tag = 0; tag < arena_tag_max; tag++) { |
| st->print(SIZE_FMT, Arena::tag_name[tag]); |
| } |
| #define HDR_FMT1 "%-8s%-8s%-10s%-8s" |
| #define HDR_FMT2 "%-6s%-19s%s" |
| |
| st->print(HDR_FMT1, "#nodes", "result", "limit", "time"); |
| st->print(HDR_FMT2, "id", "thread", "method"); |
| } |
| |
| void print_brief_oneline(outputStream* st) const { |
| int col = st->indentation(); |
| |
| // Type |
| st->print("%2s ", compilertype2name(_comp_type)); |
| col += 6; st->fill_to(col); |
| |
| // Total |
| size_t v = _peak; |
| st->print("%zu ", v); |
| col += 10; st->fill_to(col); |
| |
| for (int tag = 0; tag < arena_tag_max; tag++) { |
| v = _peak_composition_per_arena_tag[tag]; |
| st->print("%zu ", v); |
| col += 10; st->fill_to(col); |
| } |
| |
| // Number of Nodes when memory peaked |
| if (_live_nodes_at_global_peak > 0) { |
| st->print("%u ", _live_nodes_at_global_peak); |
| } else { |
| st->print("-"); |
| } |
| col += 8; st->fill_to(col); |
| |
| // result? |
| st->print("%s ", _result ? _result : ""); |
| col += 8; st->fill_to(col); |
| |
| // Limit |
| if (_limit > 0) { |
| st->print("%zu%s ", _limit, _hit_limit ? "*" : ""); |
| } else { |
| st->print("-"); |
| } |
| col += 10; st->fill_to(col); |
| |
| // TimeStamp |
| st->print("%.3f ", _time); |
| col += 8; st->fill_to(col); |
| |
| // Compile ID |
| st->print("%d ", _comp_id); |
| col += 6; st->fill_to(col); |
| |
| // Thread |
| st->print(PTR_FORMAT " ", p2i(_thread)); |
| |
| // MethodName |
| char buf[1024]; |
| st->print("%s ", _fmn.as_C_string(buf, sizeof(buf))); |
| |
| st->cr(); |
| } |
| |
| void print_detailed(outputStream* st) const { |
| int col = 0; |
| |
| constexpr int indent1 = 40; |
| constexpr int indent2 = 50; |
| |
| char buf[1024]; |
| st->print_cr("Method : %s", _fmn.as_C_string(buf, sizeof(buf))); |
| st->print_cr("Compiler : %2s", compilertype2name(_comp_type)); |
| st->print( "Arena Usage at peak : %zu", _peak); |
| if (_peak > M) { |
| st->print(" (%.2fM)", ((double)_peak/(double)M)); |
| } |
| st->cr(); |
| if (_comp_type == CompilerType::compiler_c2) { |
| st->print_cr("Nodes at peak : %u", _live_nodes_at_global_peak); |
| } |
| st->print_cr("Compile ID : %d", _comp_id); |
| st->print( "Result : %s", _result); |
| if (strcmp(_result, "oom") == 0) { |
| st->print(" (memory limit was: %zu)", _limit); |
| } |
| st->cr(); |
| st->print_cr("Thread : " PTR_FORMAT, p2i(_thread)); |
| st->print_cr("Timestamp : %.3f", _time); |
| |
| if (_detail_stats != nullptr) { |
| st->cr(); |
| st->print_cr("Arena Usage by Arena Type and compilation phase, at arena usage peak of %zu:", _peak); |
| _detail_stats->counters_at_global_peak.print_on(st); |
| st->cr(); |
| st->print_cr("Allocation timelime by phase:"); |
| _detail_stats->timeline.print_on(st); |
| } else { |
| st->cr(); |
| st->print_cr("Arena Usage by Arena Type, at arena usage peak of %zu:", _peak); |
| for (int tag = 0; tag < arena_tag_max; tag++) { |
| const size_t v = _peak_composition_per_arena_tag[tag]; |
| if (v > 0) { |
| st->print_cr("%-36s: %zu ", Arena::tag_desc[tag], v); |
| } |
| } |
| } |
| } |
| }; |
| |
| class MemStatStore : public CHeapObj<mtCompiler> { |
| |
| // Total number of entries. Reaching this limit, we discard the least interesting (smallest allocation size) first. |
| static constexpr int max_entries = 64; |
| |
| struct { |
| size_t s; MemStatEntry* e; |
| } _entries[max_entries]; |
| |
| struct iteration_result { unsigned num, num_c1, num_c2, num_filtered_out; }; |
| template<typename F> |
| void iterate_sorted_filtered(F f, size_t minsize, int max_num_printed, iteration_result& result) const { |
| assert_lock_strong(NMTCompilationCostHistory_lock); |
| const unsigned stop_after = max_num_printed == -1 ? UINT_MAX : (unsigned)max_num_printed; |
| result.num = result.num_c1 = result.num_c2 = result.num_filtered_out = 0; |
| for (int i = 0; i < max_entries && _entries[i].e != nullptr && result.num < stop_after; i++) { |
| if (_entries[i].s >= minsize) { |
| f(_entries[i].e); |
| result.num++; |
| result.num_c1 += _entries[i].e->is_c1() ? 1 : 0; |
| result.num_c2 += _entries[i].e->is_c2() ? 1 : 0; |
| } else { |
| result.num_filtered_out++; |
| } |
| } |
| } |
| |
| void print_footer(outputStream* st, size_t minsize, const iteration_result& result) const { |
| if (result.num > 0) { |
| st->print_cr("Total: %u (C1: %u, C2: %u)", result.num, result.num_c1, result.num_c2); |
| } else { |
| st->print_cr("No entries."); |
| } |
| if (result.num_filtered_out > 0) { |
| st->print_cr(" (%d compilations smaller than %zu omitted)", result.num_filtered_out, minsize); |
| } |
| } |
| |
| public: |
| |
| MemStatStore() { |
| memset(_entries, 0, sizeof(_entries)); |
| } |
| |
| void add(const ArenaStatCounter* state, const char* result) { |
| |
| const size_t size = state->peak(); |
| |
| // search insert point |
| int i = 0; |
| while (i < max_entries && _entries[i].s > size) { |
| i++; |
| } |
| if (i == max_entries) { |
| return; |
| } |
| MemStatEntry* e = _entries[max_entries - 1].e; // recycle last one |
| if (e == nullptr) { |
| e = new MemStatEntry(); |
| } |
| memmove(_entries + i + 1, _entries + i, sizeof(_entries[0]) * (max_entries - i - 1)); |
| |
| e->reset(); |
| e->set_current_time(); |
| e->set_current_thread(); |
| e->set_result(result); |
| |
| // Since we don't have phases in C1, for now we just avoid saving details for C1. |
| const bool save_details = state->comp_type() == CompilerType::compiler_c2; |
| e->set_from_state(state, save_details); |
| |
| _entries[i].s = e->peak(); |
| _entries[i].e = e; |
| } |
| |
| void print_table(outputStream* st, bool legend, size_t minsize, int max_num_printed) const { |
| assert_lock_strong(NMTCompilationCostHistory_lock); |
| |
| if (legend) { |
| MemStatEntry::print_legend(st); |
| st->cr(); |
| } |
| |
| MemStatEntry::print_header(st); |
| st->cr(); |
| |
| iteration_result itres; |
| auto printer = [&](const MemStatEntry* e) { |
| e->print_brief_oneline(st); |
| }; |
| iterate_sorted_filtered(printer, minsize, max_num_printed, itres); |
| print_footer(st, minsize, itres); |
| } |
| |
| void print_details(outputStream* st, size_t minsize, int max_num_printed) const { |
| assert_lock_strong(NMTCompilationCostHistory_lock); |
| iteration_result itres; |
| auto printer = [&](const MemStatEntry* e) { |
| e->print_detailed(st); |
| st->cr(); |
| st->print_cr("------------------------"); |
| st->cr(); |
| }; |
| iterate_sorted_filtered(printer, minsize, max_num_printed, itres); |
| } |
| }; |
| |
| bool CompilationMemoryStatistic::_enabled = false; |
| static MemStatStore* _the_store = nullptr; |
| |
| void CompilationMemoryStatistic::initialize() { |
| assert(_enabled == false && _the_store == nullptr, "Only once"); |
| _the_store = new MemStatStore; |
| _enabled = true; |
| log_info(compilation, alloc)("Compilation memory statistic enabled"); |
| } |
| |
| void CompilationMemoryStatistic::on_start_compilation(const DirectiveSet* directive) { |
| assert(enabled(), "Not enabled?"); |
| assert(directive->should_collect_memstat(), "Don't call if not needed"); |
| CompilerThread* const th = Thread::current()->as_Compiler_thread(); |
| CompileTask* const task = th->task(); |
| const size_t limit = directive->mem_limit(); |
| // Create new ArenaStat object and hook it into the thread |
| assert(th->arena_stat() == nullptr, "Sanity"); |
| ArenaStatCounter* const arena_stat = new ArenaStatCounter(task, limit); |
| th->set_arenastat(arena_stat); |
| // Start a "root" phase |
| PhaseInfo info; |
| info.id = phase_trc_id_none; |
| info.num = 0; |
| info.text = "(outside)"; |
| arena_stat->on_phase_start(info); |
| } |
| |
| void CompilationMemoryStatistic::on_end_compilation() { |
| assert(enabled(), "Not enabled?"); |
| CompilerThread* const th = Thread::current()->as_Compiler_thread(); |
| ArenaStatCounter* const arena_stat = th->arena_stat(); |
| if (arena_stat == nullptr) { // not started |
| return; |
| } |
| |
| // Mark end of compilation by clearing out the arena state object in the CompilerThread. |
| // Do this before the final "phase end". |
| th->set_arenastat(nullptr); |
| |
| // End final outer phase. |
| arena_stat->on_phase_end(); |
| |
| CompileTask* const task = th->task(); |
| assert(task->compile_id() == arena_stat->comp_id(), "Different compilation?"); |
| |
| const Method* const m = th->task()->method(); |
| |
| const DirectiveSet* directive = th->task()->directive(); |
| assert(directive->should_collect_memstat(), "Should only be called if memstat is enabled for this method"); |
| const bool print = directive->should_print_memstat(); |
| |
| // Store memory used in task, for later processing by JFR |
| task->set_arena_bytes(arena_stat->peak()); |
| |
| // Store result (ok, failed, oom...) |
| // For this to work, we must call on_end_compilation() at a point where |
| // Compile|Compilation already handed over the failure string to ciEnv, |
| // but ciEnv must still be alive. |
| const char* result = "ok"; // ok |
| const ciEnv* const env = th->env(); |
| if (env) { |
| const char* const failure_reason = env->failure_reason(); |
| if (failure_reason != nullptr) { |
| result = (strcmp(failure_reason, failure_reason_memlimit()) == 0) ? "oom" : "err"; |
| } |
| } |
| |
| { |
| MutexLocker ml(NMTCompilationCostHistory_lock, Mutex::_no_safepoint_check_flag); |
| assert(_the_store != nullptr, "not initialized"); |
| _the_store->add(arena_stat, result); |
| } |
| |
| if (print) { |
| // Pre-assemble string to prevent tearing |
| stringStream ss; |
| ss.print("%s (%d) (%s) Arena usage ", compilertype2name(arena_stat->comp_type()), arena_stat->comp_id(), result); |
| arena_stat->fmn().print_on(&ss); |
| ss.print_raw(": "); |
| arena_stat->print_peak_state_on(&ss); |
| tty->print_raw(ss.base()); |
| } |
| |
| delete arena_stat; |
| } |
| |
| static void inform_compilation_about_oom(CompilerType ct) { |
| // Inform C1 or C2 that an OOM happened. They will take delayed action |
| // and abort the compilation in progress. Note that this is not instantaneous, |
| // since the compiler has to actively bailout, which may take a while, during |
| // which memory usage may rise further. |
| // |
| // The mechanism differs slightly between C1 and C2: |
| // - With C1, we directly set the bailout string, which will cause C1 to |
| // bailout at the typical BAILOUT places. |
| // - With C2, the corresponding mechanism would be the failure string; but |
| // bailout paths in C2 are not complete and therefore it is dangerous to |
| // set the failure string at - for C2 - seemingly random places. Instead, |
| // upon OOM C2 sets the failure string next time it checks the node limit. |
| if (ciEnv::current() != nullptr) { |
| void* compiler_data = ciEnv::current()->compiler_data(); |
| #ifdef COMPILER1 |
| if (ct == compiler_c1) { |
| Compilation* C = static_cast<Compilation*>(compiler_data); |
| if (C != nullptr) { |
| C->bailout(CompilationMemoryStatistic::failure_reason_memlimit()); |
| C->set_oom(); |
| } |
| } |
| #endif |
| #ifdef COMPILER2 |
| if (ct == compiler_c2) { |
| Compile* C = static_cast<Compile*>(compiler_data); |
| if (C != nullptr) { |
| C->set_oom(); |
| } |
| } |
| #endif // COMPILER2 |
| } |
| } |
| |
| void CompilationMemoryStatistic::on_arena_chunk_allocation(size_t size, int arena_tag, uint64_t* stamp) { |
| |
| assert(enabled(), "Not enabled?"); |
| assert(arena_tag >= 0 && arena_tag < arena_tag_max, "Arena Tag OOB (%d)", arena_tag_max); |
| |
| CompilerThread* const th = Thread::current()->as_Compiler_thread(); |
| ArenaStatCounter* const arena_stat = th->arena_stat(); |
| if (arena_stat == nullptr || // not started |
| arena_stat->limit_in_process()) { // limit handling in process, prevent recursion |
| return; |
| } |
| |
| // Compiler can be slow to bailout, so we may hit memlimit more than once |
| const bool hit_limit_before = arena_stat->hit_limit(); |
| |
| if (arena_stat->on_arena_chunk_allocation(size, arena_tag, stamp)) { // new peak? |
| |
| // Limit handling |
| if (arena_stat->hit_limit()) { |
| char name[1024] = ""; |
| const CompilerType ct = arena_stat->comp_type(); |
| const bool print = arena_stat->should_print_memstat(); |
| const bool crash = arena_stat->should_crash_on_memlimit(); |
| arena_stat->set_limit_in_process(true); // prevent recursive limit hits |
| arena_stat->fmn().as_C_string(name, sizeof(name)); |
| |
| inform_compilation_about_oom(ct); |
| |
| if (crash) { |
| // Store this ArenaStat. If other threads also run into OOMs, let them sleep. |
| // We will never return, so the global store will not contain this info. We will |
| // print the stored ArenaStat in hs-err (see print_error_report) |
| if (Atomic::cmpxchg(&_arenastat_oom_crash, (ArenaStatCounter*) nullptr, arena_stat) != nullptr) { |
| os::infinite_sleep(); |
| } |
| } |
| |
| stringStream short_msg; |
| |
| // We print to tty if either print is enabled or if we are to crash on MemLimit hit. |
| // If printing/crashing are not enabled, we just quietly abort the compilation. The |
| // compilation is marked as "oom" in the compilation memory result store. |
| if (print || crash) { |
| short_msg.print("%s (%d) %s: ", compilertype2name(ct), arena_stat->comp_id(), name); |
| short_msg.print("Hit MemLimit %s- limit: %zu now: %zu", |
| (hit_limit_before ? "again " : ""), |
| arena_stat->limit(), arena_stat->peak()); |
| tty->print_raw(short_msg.base()); |
| tty->cr(); |
| } |
| |
| if (crash) { |
| // Before crashing, if C2, end current phase. That causes its info (which is the most important) to |
| // be added to the phase timeline. |
| if (arena_stat->comp_type() == CompilerType::compiler_c2) { |
| arena_stat->on_phase_end(); |
| } |
| // print extended message to tty (mirrors the one that should show up in the hs-err file, just for good measure) |
| tty->print_cr("Details:"); |
| arena_stat->print_peak_state_on(tty); |
| tty->cr(); |
| // abort VM |
| report_fatal(OOM_HOTSPOT_ARENA, __FILE__, __LINE__, "%s", short_msg.base()); |
| } |
| |
| arena_stat->set_limit_in_process(false); |
| } // end Limit handling |
| } |
| } |
| |
| void CompilationMemoryStatistic::on_arena_chunk_deallocation(size_t size, uint64_t stamp) { |
| assert(enabled(), "Not enabled?"); |
| CompilerThread* const th = Thread::current()->as_Compiler_thread(); |
| ArenaStatCounter* const arena_stat = th->arena_stat(); |
| if (arena_stat == nullptr) { // not started |
| return; |
| } |
| if (arena_stat->limit_in_process()) { |
| return; // avoid recursion on limit hit |
| } |
| |
| arena_stat->on_arena_chunk_deallocation(size, stamp); |
| } |
| |
| void CompilationMemoryStatistic::on_phase_start(int phase_trc_id, const char* text) { |
| assert(enabled(), "Not enabled?"); |
| assert(phase_trc_id >= 0 && phase_trc_id < phase_trc_id_max, "Phase trace id OOB (%d)", phase_trc_id); |
| CompilerThread* const th = Thread::current()->as_Compiler_thread(); |
| ArenaStatCounter* const arena_stat = th->arena_stat(); |
| if (arena_stat == nullptr) { // not started |
| return; |
| } |
| PhaseInfo info; |
| info.id = phase_trc_id; |
| info.num = arena_stat->advance_phase_counter(); |
| info.text = text; |
| arena_stat->on_phase_start(info); |
| } |
| |
| void CompilationMemoryStatistic::on_phase_end() { |
| assert(enabled(), "Not enabled?"); |
| CompilerThread* const th = Thread::current()->as_Compiler_thread(); |
| ArenaStatCounter* const arena_stat = th->arena_stat(); |
| if (arena_stat == nullptr) { // not started |
| return; |
| } |
| arena_stat->on_phase_end(); |
| } |
| |
| static bool check_before_reporting(outputStream* st) { |
| if (!CompilationMemoryStatistic::enabled()) { |
| st->print_cr("Compilation memory statistics disabled."); |
| return false; |
| } |
| if (_the_store == nullptr) { |
| st->print_cr("Compilation memory statistics not yet initialized. "); |
| return false; |
| } |
| return true; |
| } |
| |
| bool CompilationMemoryStatistic::in_oom_crash() { |
| return Atomic::load(&_arenastat_oom_crash) != nullptr; |
| } |
| |
| void CompilationMemoryStatistic::print_error_report(outputStream* st) { |
| if (!check_before_reporting(st)) { |
| return; |
| } |
| StreamIndentor si(tty, 4); |
| const ArenaStatCounter* const oom_stats = Atomic::load(&_arenastat_oom_crash); |
| if (oom_stats != nullptr) { |
| // we crashed due to a compiler limit hit. Lead with a printout of the offending stats |
| // in detail. |
| st->print_cr("Compiler Memory Statistic, hit OOM limit; offending compilation:"); |
| oom_stats->fmn().print_on(st); |
| st->cr(); |
| oom_stats->print_peak_state_on(st); |
| st->cr(); |
| } |
| st->print_cr("Compiler Memory Statistic, 10 most expensive compilations:"); |
| print_all_by_size(st, false, false, 0, 10); |
| } |
| |
| void CompilationMemoryStatistic::print_final_report(outputStream* st) { |
| if (!check_before_reporting(st)) { |
| return; |
| } |
| st->print_cr("Compiler Memory Statistic, 10 most expensive compilations:"); |
| StreamIndentor si(st, 4); |
| print_all_by_size(st, false, false, 0, 10); |
| } |
| |
| void CompilationMemoryStatistic::print_jcmd_report(outputStream* st, bool verbose, bool legend, size_t minsize) { |
| if (!check_before_reporting(st)) { |
| return; |
| } |
| st->print_cr("Compiler Memory Statistic"); |
| StreamIndentor si(st, 4); |
| print_all_by_size(st, verbose, legend, minsize, -1); |
| } |
| |
| void CompilationMemoryStatistic::print_all_by_size(outputStream* st, bool verbose, bool legend, size_t minsize, int max_num_printed) { |
| MutexLocker ml(NMTCompilationCostHistory_lock, Mutex::_no_safepoint_check_flag); |
| if (verbose) { |
| _the_store->print_details(st, minsize, max_num_printed); |
| } else { |
| _the_store->print_table(st, legend, minsize, max_num_printed); |
| } |
| } |
| |
| const char* CompilationMemoryStatistic::failure_reason_memlimit() { |
| static const char* const s = "hit memory limit while compiling"; |
| return s; |
| } |
| |
| #ifdef ASSERT |
| void CompilationMemoryStatistic::do_test_allocations() { |
| |
| CompilerThread* const th = Thread::current()->as_Compiler_thread(); |
| ArenaStatCounter* const arena_stat = th->arena_stat(); |
| if (arena_stat == nullptr || !arena_stat->is_test_class()) { // not started or not the JTREG test |
| return; |
| } |
| const CompilerType ctyp = th->task()->compiler()->type(); |
| |
| // Note: all allocations in here need to be tagged as mtCompiler to be recognized |
| // by the compilation memstat. ResourceArea in a CompilerThread is already tagged |
| // mtCompiler (special handling for compiler threads). |
| |
| #ifdef COMPILER2 |
| if (ctyp == CompilerType::compiler_c2) { |
| { |
| Compile::TracePhase tp(Phase::_t_testPhase1); |
| Arena ar(MemTag::mtCompiler, Arena::Tag::tag_reglive); |
| ar.Amalloc(2 * M); // phase-local peak |
| } |
| { |
| Compile::TracePhase tp(Phase::_t_testPhase2); |
| NEW_RESOURCE_ARRAY(char, 32 * M); // leaked (until compilation end) |
| } |
| } // C2 |
| #endif // COMPILER2 |
| |
| #ifdef COMPILER1 |
| if (ctyp == CompilerType::compiler_c1) { |
| NEW_RESOURCE_ARRAY(char, 32 * M); // leaked (until compilation end) |
| } |
| #endif // COMPILER2 |
| |
| } |
| #endif // ASSERT |
| |
| CompilationMemoryStatisticMark::CompilationMemoryStatisticMark(const DirectiveSet* directive) |
| : _active(directive->should_collect_memstat()) { |
| if (_active) { |
| CompilationMemoryStatistic::on_start_compilation(directive); |
| } |
| } |
| |
| CompilationMemoryStatisticMark::~CompilationMemoryStatisticMark() { |
| if (_active) { |
| CompilationMemoryStatistic::on_end_compilation(); |
| } |
| } |