8caeaf7a6c
This CL introduces new embedding APIs for supporting heap sample profiling. A registered sampling callback is invoked approximately every N bytes based on an exponential distribution, providing information about the isolate group the allocation occurred in, the user visible name of the allocated object type, a weak persistent handle to the allocated object, and the size of the allocation. Sampling is triggered using artificial TLAB boundaries to cause allocations to be sampled to take the allocation slow path where the registered callback can be invoked with the allocation information. Only new space allocations are currently traced, with old space allocation support to be added in a future CL. TEST=Dart_HeapSampling Change-Id: I22bcdeec6e823bc1ab44898d4c596fbed7169fa1 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/264520 Commit-Queue: Ben Konyi <bkonyi@google.com> Reviewed-by: Siva Annamalai <asiva@google.com> Reviewed-by: Ryan Macnak <rmacnak@google.com>
585 lines
18 KiB
C++
585 lines
18 KiB
C++
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
#ifndef RUNTIME_VM_CLASS_TABLE_H_
|
|
#define RUNTIME_VM_CLASS_TABLE_H_
|
|
|
|
#include <memory>
|
|
#include <tuple>
|
|
#include <utility>
|
|
|
|
#include "platform/allocation.h"
|
|
#include "platform/assert.h"
|
|
#include "platform/atomic.h"
|
|
#include "platform/utils.h"
|
|
|
|
#include "vm/bitfield.h"
|
|
#include "vm/class_id.h"
|
|
#include "vm/flags.h"
|
|
#include "vm/globals.h"
|
|
#include "vm/tagged_pointer.h"
|
|
|
|
namespace dart {
|
|
|
|
class Class;
|
|
class ClassTable;
|
|
class Isolate;
|
|
class IsolateGroup;
|
|
class JSONArray;
|
|
class JSONObject;
|
|
class JSONStream;
|
|
template <typename T>
|
|
class MallocGrowableArray;
|
|
class ObjectPointerVisitor;
|
|
class PersistentHandle;
|
|
|
|
// A 64-bit bitmap describing unboxed fields in a class.
|
|
//
|
|
// There is a bit for each word in an instance of the class.
|
|
//
|
|
// Words corresponding to set bits must be ignored by the GC because they
|
|
// don't contain pointers. All words beyound the first 64 words of an object
|
|
// are expected to contain pointers.
|
|
class UnboxedFieldBitmap {
|
|
public:
|
|
UnboxedFieldBitmap() : bitmap_(0) {}
|
|
explicit UnboxedFieldBitmap(uint64_t bitmap) : bitmap_(bitmap) {}
|
|
UnboxedFieldBitmap(const UnboxedFieldBitmap&) = default;
|
|
UnboxedFieldBitmap& operator=(const UnboxedFieldBitmap&) = default;
|
|
|
|
DART_FORCE_INLINE bool Get(intptr_t position) const {
|
|
if (position >= Length()) return false;
|
|
return Utils::TestBit(bitmap_, position);
|
|
}
|
|
DART_FORCE_INLINE void Set(intptr_t position) {
|
|
ASSERT(position < Length());
|
|
bitmap_ |= Utils::Bit<decltype(bitmap_)>(position);
|
|
}
|
|
DART_FORCE_INLINE void Clear(intptr_t position) {
|
|
ASSERT(position < Length());
|
|
bitmap_ &= ~Utils::Bit<decltype(bitmap_)>(position);
|
|
}
|
|
DART_FORCE_INLINE uint64_t Value() const { return bitmap_; }
|
|
DART_FORCE_INLINE bool IsEmpty() const { return bitmap_ == 0; }
|
|
DART_FORCE_INLINE void Reset() { bitmap_ = 0; }
|
|
|
|
DART_FORCE_INLINE static constexpr intptr_t Length() {
|
|
return sizeof(decltype(bitmap_)) * kBitsPerByte;
|
|
}
|
|
|
|
private:
|
|
uint64_t bitmap_;
|
|
};
|
|
|
|
// Allocator used to manage memory for ClassTable arrays and ClassTable
|
|
// objects themselves.
|
|
//
|
|
// This allocator provides delayed free functionality: normally class tables
|
|
// can't be freed unless all mutator and helper threads are stopped because
|
|
// some of these threads might be holding a pointer to a table which we
|
|
// want to free. Instead of stopping the world whenever we need to free
|
|
// a table (e.g. freeing old table after growing) we delay freeing until an
|
|
// occasional GC which will need to stop the world anyway.
|
|
class ClassTableAllocator : public ValueObject {
|
|
public:
|
|
ClassTableAllocator();
|
|
~ClassTableAllocator();
|
|
|
|
// Allocate an array of T with |len| elements.
|
|
//
|
|
// Does *not* initialize the memory.
|
|
template <class T>
|
|
inline T* Alloc(intptr_t len) {
|
|
return reinterpret_cast<T*>(dart::malloc(len * sizeof(T)));
|
|
}
|
|
|
|
// Allocate a zero initialized array of T with |len| elements.
|
|
template <class T>
|
|
inline T* AllocZeroInitialized(intptr_t len) {
|
|
return reinterpret_cast<T*>(dart::calloc(len, sizeof(T)));
|
|
}
|
|
|
|
// Clone the given |array| with |size| elements.
|
|
template <class T>
|
|
inline T* Clone(T* array, intptr_t size) {
|
|
if (array == nullptr) {
|
|
ASSERT(size == 0);
|
|
return nullptr;
|
|
}
|
|
auto result = Alloc<T>(size);
|
|
memmove(result, array, size * sizeof(T));
|
|
return result;
|
|
}
|
|
|
|
// Copy |size| elements from the given |array| into a new
|
|
// array with space for |new_size| elements. Then |Free|
|
|
// the original |array|.
|
|
//
|
|
// |new_size| is expected to be larger than |size|.
|
|
template <class T>
|
|
inline T* Realloc(T* array, intptr_t size, intptr_t new_size) {
|
|
ASSERT(size < new_size);
|
|
auto result = AllocZeroInitialized<T>(new_size);
|
|
if (size != 0) {
|
|
ASSERT(result != nullptr);
|
|
memmove(result, array, size * sizeof(T));
|
|
}
|
|
Free(array);
|
|
return result;
|
|
}
|
|
|
|
// Schedule deletion of the given ClassTable.
|
|
void Free(ClassTable* table);
|
|
|
|
// Schedule freeing of the given pointer.
|
|
void Free(void* ptr);
|
|
|
|
// Free all objects which were scheduled by |Free|. Expected to only be
|
|
// called on |IsolateGroup| shutdown or when the world is stopped and no
|
|
// thread can be using a stale class table pointer.
|
|
void FreePending();
|
|
|
|
private:
|
|
typedef void (*Deleter)(void*);
|
|
MallocGrowableArray<std::pair<void*, Deleter>>* pending_freed_;
|
|
};
|
|
|
|
// A table with the given |Columns| indexed by class id.
|
|
//
|
|
// Each column is a continous array of a the given type. All columns have
|
|
// the same number of used elements (|num_cids()|) and the same capacity.
|
|
template <typename CidType, typename... Columns>
|
|
class CidIndexedTable {
|
|
public:
|
|
explicit CidIndexedTable(ClassTableAllocator* allocator)
|
|
: allocator_(allocator) {}
|
|
|
|
~CidIndexedTable() {
|
|
std::apply([&](auto&... column) { (allocator_->Free(column.load()), ...); },
|
|
columns_);
|
|
}
|
|
|
|
CidIndexedTable(const CidIndexedTable& other) = delete;
|
|
|
|
void SetNumCidsAndCapacity(intptr_t new_num_cids, intptr_t new_capacity) {
|
|
columns_ = std::apply(
|
|
[&](auto&... column) {
|
|
return std::make_tuple(
|
|
allocator_->Realloc(column.load(), num_cids_, new_capacity)...);
|
|
},
|
|
columns_);
|
|
capacity_ = new_capacity;
|
|
SetNumCids(new_num_cids);
|
|
}
|
|
|
|
void AllocateIndex(intptr_t index, bool* did_grow) {
|
|
*did_grow = EnsureCapacity(index);
|
|
SetNumCids(Utils::Maximum(num_cids_, index + 1));
|
|
}
|
|
|
|
intptr_t AddRow(bool* did_grow) {
|
|
*did_grow = EnsureCapacity(num_cids_);
|
|
intptr_t id = num_cids_;
|
|
SetNumCids(num_cids_ + 1);
|
|
return id;
|
|
}
|
|
|
|
void ShrinkTo(intptr_t new_num_cids) {
|
|
ASSERT(new_num_cids <= num_cids_);
|
|
num_cids_ = new_num_cids;
|
|
}
|
|
|
|
bool IsValidIndex(intptr_t index) const {
|
|
return 0 <= index && index < num_cids_;
|
|
}
|
|
|
|
void CopyFrom(const CidIndexedTable& other) {
|
|
ASSERT(allocator_ == other.allocator_);
|
|
|
|
std::apply([&](auto&... column) { (allocator_->Free(column.load()), ...); },
|
|
columns_);
|
|
|
|
columns_ = std::apply(
|
|
[&](auto&... column) {
|
|
return std::make_tuple(
|
|
allocator_->Clone(column.load(), other.num_cids_)...);
|
|
},
|
|
other.columns_);
|
|
capacity_ = num_cids_ = other.num_cids_;
|
|
}
|
|
|
|
void Remap(intptr_t* old_to_new_cid) {
|
|
CidIndexedTable clone(allocator_);
|
|
clone.CopyFrom(*this);
|
|
RemapAllColumns(clone, old_to_new_cid,
|
|
std::index_sequence_for<Columns...>{});
|
|
}
|
|
|
|
template <
|
|
intptr_t kColumnIndex,
|
|
typename T = std::tuple_element_t<kColumnIndex, std::tuple<Columns...>>>
|
|
T* GetColumn() {
|
|
return std::get<kColumnIndex>(columns_).load();
|
|
}
|
|
|
|
template <
|
|
intptr_t kColumnIndex,
|
|
typename T = std::tuple_element_t<kColumnIndex, std::tuple<Columns...>>>
|
|
const T* GetColumn() const {
|
|
return std::get<kColumnIndex>(columns_).load();
|
|
}
|
|
|
|
template <
|
|
intptr_t kColumnIndex,
|
|
typename T = std::tuple_element_t<kColumnIndex, std::tuple<Columns...>>>
|
|
T& At(intptr_t index) {
|
|
ASSERT(IsValidIndex(index));
|
|
return GetColumn<kColumnIndex>()[index];
|
|
}
|
|
|
|
template <
|
|
intptr_t kColumnIndex,
|
|
typename T = std::tuple_element_t<kColumnIndex, std::tuple<Columns...>>>
|
|
const T& At(intptr_t index) const {
|
|
ASSERT(IsValidIndex(index));
|
|
return GetColumn<kColumnIndex>()[index];
|
|
}
|
|
|
|
intptr_t num_cids() const { return num_cids_; }
|
|
intptr_t capacity() const { return capacity_; }
|
|
|
|
private:
|
|
friend class ClassTable;
|
|
|
|
// Wrapper around AcqRelAtomic<T*> which makes it assignable and copyable
|
|
// so that we could put it inside an std::tuple.
|
|
template <typename T>
|
|
struct Ptr {
|
|
Ptr() : ptr(nullptr) {}
|
|
Ptr(T* ptr) : ptr(ptr) {} // NOLINT
|
|
|
|
Ptr(const Ptr& other) { ptr.store(other.ptr.load()); }
|
|
|
|
Ptr& operator=(const Ptr& other) {
|
|
ptr.store(other.load());
|
|
return *this;
|
|
}
|
|
|
|
T* load() const { return ptr.load(); }
|
|
|
|
AcqRelAtomic<T*> ptr = {nullptr};
|
|
};
|
|
|
|
void SetNumCids(intptr_t new_num_cids) {
|
|
if (new_num_cids > kClassIdTagMax) {
|
|
FATAL("Too many classes");
|
|
}
|
|
num_cids_ = new_num_cids;
|
|
}
|
|
|
|
bool EnsureCapacity(intptr_t index) {
|
|
if (index >= capacity_) {
|
|
SetNumCidsAndCapacity(num_cids_, index + kCapacityIncrement);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
template <intptr_t kColumnIndex>
|
|
void RemapColumn(const CidIndexedTable& old, intptr_t* old_to_new_cid) {
|
|
auto new_column = GetColumn<kColumnIndex>();
|
|
auto old_column = old.GetColumn<kColumnIndex>();
|
|
for (intptr_t i = 0; i < num_cids_; i++) {
|
|
new_column[old_to_new_cid[i]] = old_column[i];
|
|
}
|
|
}
|
|
|
|
template <std::size_t... Is>
|
|
void RemapAllColumns(const CidIndexedTable& old,
|
|
intptr_t* old_to_new_cid,
|
|
std::index_sequence<Is...>) {
|
|
(RemapColumn<Is>(old, old_to_new_cid), ...);
|
|
}
|
|
|
|
static constexpr intptr_t kCapacityIncrement = 256;
|
|
|
|
ClassTableAllocator* allocator_;
|
|
intptr_t num_cids_ = 0;
|
|
intptr_t capacity_ = 0;
|
|
std::tuple<Ptr<Columns>...> columns_;
|
|
};
|
|
|
|
// Registry of all known classes.
|
|
//
|
|
// The GC will only use information about instance size and unboxed field maps
|
|
// to scan instances and will not access class objects themselves. This
|
|
// information is stored in separate columns of the |classes_| table.
|
|
//
|
|
// # Concurrency & atomicity
|
|
//
|
|
// This table is read concurrently without locking (e.g. by GC threads) so
|
|
// there are some invariants that need to be observed when working with it.
|
|
//
|
|
// * When table is updated (e.g. when the table is grown or a new class is
|
|
// registered in a table) there must be a release barrier after the update.
|
|
// Such barrier will ensure that stores which populate the table are not
|
|
// reordered past the store which exposes the new grown table or exposes
|
|
// a new class id;
|
|
// * Old versions of the table can only be freed when the world is stopped:
|
|
// no mutator and no helper threads are running. To avoid freeing a table
|
|
// which some other thread is reading from.
|
|
//
|
|
// Note that torn reads are not a concern (e.g. it is fine to use
|
|
// memmove to copy class table contents) as long as an appropriate
|
|
// barrier is issued before the copy of the table can be observed.
|
|
//
|
|
// # Hot reload
|
|
//
|
|
// Each IsolateGroup contains two ClassTable fields: |class_table| and
|
|
// |heap_walk_class_table|. GC visitors use the second field to get ClassTable
|
|
// instance which they will use for visiting pointers inside instances in
|
|
// the heap. Usually these two fields will be pointing to the same table,
|
|
// except when IsolateGroup is in the middle of reload.
|
|
//
|
|
// When reloading |class_table| will be pointing to a copy of the original
|
|
// table. Kernel loading will be modifying this table, while GC
|
|
// workers can continue using original table still available through
|
|
// |heap_walk_class_table|. If hot reload succeeds, |heap_walk_class_table|
|
|
// will be dropped and |class_table| will become the source of truth. Otherwise,
|
|
// original table will be restored from |heap_walk_class_table|.
|
|
//
|
|
// See IsolateGroup methods CloneClassTableForReload, RestoreOriginalClassTable,
|
|
// DropOriginalClassTable.
|
|
class ClassTable : public MallocAllocated {
|
|
public:
|
|
explicit ClassTable(ClassTableAllocator* allocator);
|
|
|
|
~ClassTable();
|
|
|
|
ClassTable* Clone() const { return new ClassTable(*this); }
|
|
|
|
ClassPtr At(intptr_t cid) const {
|
|
if (IsTopLevelCid(cid)) {
|
|
return top_level_classes_.At<kClassIndex>(IndexFromTopLevelCid(cid));
|
|
}
|
|
return classes_.At<kClassIndex>(cid);
|
|
}
|
|
|
|
int32_t SizeAt(intptr_t index) const {
|
|
if (IsTopLevelCid(index)) {
|
|
return 0;
|
|
}
|
|
return classes_.At<kSizeIndex>(index);
|
|
}
|
|
|
|
void SetAt(intptr_t index, ClassPtr raw_cls);
|
|
void UpdateClassSize(intptr_t cid, ClassPtr raw_cls);
|
|
|
|
bool IsValidIndex(intptr_t cid) const {
|
|
if (IsTopLevelCid(cid)) {
|
|
return top_level_classes_.IsValidIndex(IndexFromTopLevelCid(cid));
|
|
}
|
|
return classes_.IsValidIndex(cid);
|
|
}
|
|
|
|
bool HasValidClassAt(intptr_t cid) const { return At(cid) != nullptr; }
|
|
|
|
UnboxedFieldBitmap GetUnboxedFieldsMapAt(intptr_t cid) const {
|
|
ASSERT(IsValidIndex(cid));
|
|
return classes_.At<kUnboxedFieldBitmapIndex>(cid);
|
|
}
|
|
|
|
void SetUnboxedFieldsMapAt(intptr_t cid, UnboxedFieldBitmap map) {
|
|
ASSERT(IsValidIndex(cid));
|
|
classes_.At<kUnboxedFieldBitmapIndex>(cid) = map;
|
|
}
|
|
|
|
#if !defined(PRODUCT)
|
|
bool ShouldTraceAllocationFor(intptr_t cid) {
|
|
return !IsTopLevelCid(cid) &&
|
|
(classes_.At<kAllocationTracingStateIndex>(cid) != kTracingDisabled);
|
|
}
|
|
|
|
void SetTraceAllocationFor(intptr_t cid, bool trace) {
|
|
classes_.At<kAllocationTracingStateIndex>(cid) =
|
|
trace ? kTraceAllocationBit : kTracingDisabled;
|
|
}
|
|
|
|
void SetCollectInstancesFor(intptr_t cid, bool trace) {
|
|
auto& slot = classes_.At<kAllocationTracingStateIndex>(cid);
|
|
if (trace) {
|
|
slot |= kCollectInstancesBit;
|
|
} else {
|
|
slot &= ~kCollectInstancesBit;
|
|
}
|
|
}
|
|
|
|
bool CollectInstancesFor(intptr_t cid) {
|
|
auto& slot = classes_.At<kAllocationTracingStateIndex>(cid);
|
|
return (slot & kCollectInstancesBit) != 0;
|
|
}
|
|
|
|
void UpdateCachedAllocationTracingStateTablePointer() {
|
|
cached_allocation_tracing_state_table_.store(
|
|
classes_.GetColumn<kAllocationTracingStateIndex>());
|
|
}
|
|
|
|
PersistentHandle* UserVisibleNameFor(intptr_t cid) {
|
|
return classes_.At<kClassNameIndex>(cid);
|
|
}
|
|
|
|
void SetUserVisibleNameFor(intptr_t cid, PersistentHandle* name) {
|
|
classes_.At<kClassNameIndex>(cid) = name;
|
|
}
|
|
#else
|
|
void UpdateCachedAllocationTracingStateTablePointer() {}
|
|
#endif // !defined(PRODUCT)
|
|
|
|
intptr_t NumCids() const {
|
|
return classes_.num_cids();
|
|
}
|
|
intptr_t Capacity() const {
|
|
return classes_.capacity();
|
|
}
|
|
|
|
intptr_t NumTopLevelCids() const {
|
|
return top_level_classes_.num_cids();
|
|
}
|
|
|
|
void Register(const Class& cls);
|
|
void AllocateIndex(intptr_t index);
|
|
|
|
void RegisterTopLevel(const Class& cls);
|
|
void UnregisterTopLevel(intptr_t index);
|
|
|
|
void Remap(intptr_t* old_to_new_cids);
|
|
|
|
void VisitObjectPointers(ObjectPointerVisitor* visitor);
|
|
|
|
// If a snapshot reader has populated the class table then the
|
|
// sizes in the class table are not correct. Iterates through the
|
|
// table, updating the sizes.
|
|
void CopySizesFromClassObjects();
|
|
|
|
void Validate();
|
|
|
|
void Print();
|
|
|
|
#if defined(DART_PRECOMPILER)
|
|
void PrintObjectLayout(const char* filename);
|
|
#endif
|
|
|
|
#ifndef PRODUCT
|
|
// Describes layout of heap stats for code generation. See offset_extractor.cc
|
|
struct ArrayTraits {
|
|
static intptr_t elements_start_offset() { return 0; }
|
|
|
|
static constexpr intptr_t kElementSize = sizeof(uint8_t);
|
|
};
|
|
|
|
static intptr_t allocation_tracing_state_table_offset() {
|
|
static_assert(sizeof(cached_allocation_tracing_state_table_) == kWordSize);
|
|
return OFFSET_OF(ClassTable, cached_allocation_tracing_state_table_);
|
|
}
|
|
|
|
void AllocationProfilePrintJSON(JSONStream* stream, bool internal);
|
|
|
|
void PrintToJSONObject(JSONObject* object);
|
|
#endif // !PRODUCT
|
|
|
|
// Deallocates table copies. Do not call during concurrent access to table.
|
|
void FreeOldTables();
|
|
|
|
static bool IsTopLevelCid(intptr_t cid) { return cid >= kTopLevelCidOffset; }
|
|
|
|
static intptr_t IndexFromTopLevelCid(intptr_t cid) {
|
|
ASSERT(IsTopLevelCid(cid));
|
|
return cid - kTopLevelCidOffset;
|
|
}
|
|
|
|
static intptr_t CidFromTopLevelIndex(intptr_t index) {
|
|
return kTopLevelCidOffset + index;
|
|
}
|
|
|
|
private:
|
|
friend class ClassTableAllocator;
|
|
friend class GCMarker;
|
|
friend class MarkingWeakVisitor;
|
|
friend class Scavenger;
|
|
friend class ScavengerWeakVisitor;
|
|
friend class Dart;
|
|
friend Isolate* CreateWithinExistingIsolateGroup(IsolateGroup* group,
|
|
const char* name,
|
|
char** error);
|
|
friend class IsolateGroup; // for table()
|
|
static const int kInitialCapacity = 512;
|
|
|
|
static const intptr_t kTopLevelCidOffset = kClassIdTagMax + 1;
|
|
|
|
ClassTable(const ClassTable& original)
|
|
: allocator_(original.allocator_),
|
|
classes_(original.allocator_),
|
|
top_level_classes_(original.allocator_) {
|
|
classes_.CopyFrom(original.classes_);
|
|
top_level_classes_.CopyFrom(original.top_level_classes_);
|
|
UpdateCachedAllocationTracingStateTablePointer();
|
|
}
|
|
|
|
void AllocateTopLevelIndex(intptr_t index);
|
|
|
|
ClassPtr* table() {
|
|
return classes_.GetColumn<kClassIndex>();
|
|
}
|
|
|
|
// Used to drop recently added classes.
|
|
void SetNumCids(intptr_t num_cids, intptr_t num_tlc_cids) {
|
|
classes_.ShrinkTo(num_cids);
|
|
top_level_classes_.ShrinkTo(num_tlc_cids);
|
|
}
|
|
|
|
ClassTableAllocator* allocator_;
|
|
|
|
// Unfortunately std::tuple used by CidIndexedTable does not have a stable
|
|
// layout so we can't refer to its elements from generated code.
|
|
NOT_IN_PRODUCT(AcqRelAtomic<uint8_t*> cached_allocation_tracing_state_table_ =
|
|
{nullptr});
|
|
|
|
enum {
|
|
kClassIndex = 0,
|
|
kSizeIndex,
|
|
kUnboxedFieldBitmapIndex,
|
|
#if !defined(PRODUCT)
|
|
kAllocationTracingStateIndex,
|
|
kClassNameIndex,
|
|
#endif
|
|
};
|
|
|
|
#if !defined(PRODUCT)
|
|
CidIndexedTable<ClassIdTagType,
|
|
ClassPtr,
|
|
uint32_t,
|
|
UnboxedFieldBitmap,
|
|
uint8_t,
|
|
PersistentHandle*>
|
|
classes_;
|
|
#else
|
|
CidIndexedTable<ClassIdTagType, ClassPtr, uint32_t, UnboxedFieldBitmap>
|
|
classes_;
|
|
#endif
|
|
|
|
#ifndef PRODUCT
|
|
enum {
|
|
kTracingDisabled = 0,
|
|
kTraceAllocationBit = (1 << 0),
|
|
kCollectInstancesBit = (1 << 1),
|
|
};
|
|
#endif // !PRODUCT
|
|
|
|
CidIndexedTable<classid_t, ClassPtr> top_level_classes_;
|
|
};
|
|
|
|
} // namespace dart
|
|
|
|
#endif // RUNTIME_VM_CLASS_TABLE_H_
|