2 +----------------------------------------------------------------------+
4 +----------------------------------------------------------------------+
5 | Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) |
6 | Copyright (c) 1997-2010 The PHP Group |
7 +----------------------------------------------------------------------+
8 | This source file is subject to version 3.01 of the PHP license, |
9 | that is bundled with this package in the file LICENSE, and is |
10 | available through the world-wide-web at the following url: |
11 | http://www.php.net/license/3_01.txt |
12 | If you did not receive a copy of the PHP license and are unable to |
13 | obtain it through the world-wide-web, please send a note to |
14 | license@php.net so we can mail you a copy immediately. |
15 +----------------------------------------------------------------------+
18 #include "hphp/runtime/ext/extension.h"
19 #include "hphp/runtime/base/builtin-functions.h"
20 #include <folly/Format.h>
24 * Set of methods to wrap around HHVM's heap graph implementation
27 * Create a heap graph in HHVM and uses it as a Resource with a
28 * set of functions that can operate on it.
31 * Call heapgraph_create, and then any of the other heapgraph
37 #include <unordered_map>
39 #include <boost/dynamic_bitset.hpp>
41 #include "hphp/runtime/base/array-init.h"
42 #include "hphp/runtime/base/collections.h"
43 #include "hphp/runtime/base/memory-manager-defs.h"
44 #include "hphp/runtime/ext/datetime/ext_datetime.h"
45 #include "hphp/runtime/ext/simplexml/ext_simplexml.h"
46 #include "hphp/runtime/ext/std/ext_std_closure.h"
47 #include "hphp/runtime/vm/class.h"
48 #include "hphp/runtime/vm/unit.h"
49 #include "hphp/util/alloc.h"
50 #include "hphp/runtime/base/heap-graph.h"
51 #include "hphp/runtime/base/heap-algorithms.h"
52 #include "hphp/runtime/base/container-functions.h"
53 #include "hphp/runtime/base/rds.h"
54 #include "hphp/runtime/base/zend-string.h"
55 #include "hphp/runtime/vm/vm-regs.h"
64 TRACE_SET_MOD(heapgraph
);
66 ///////////////////////////////////////////////////////////////////////////////
72 s_root_nodes("root_nodes"),
73 s_root_path("root_path"),
97 ///////////////////////////////////////////////////////////////////////////////
100 // Extra information about a HeapGraph::Node.
102 CapturedNode() : static_local() {}
103 CapturedNode(const CapturedNode
& other
) {
104 memcpy(this, &other
, sizeof other
);
106 rds::StaticLocal static_local
; // only for rds::StaticLocalData
107 rds::SPropCache sprop_cache
; // only for HPHP::SPropCache
111 } heap_object
; // only for non-roots
114 // Extra information about a HeapGraph::Ptr
116 enum { Key
, Value
, Property
, Offset
} index_kind
;
117 uint32_t index
; // location of ptr within it's from node
120 struct HeapGraphContext
: SweepableResourceData
{
121 explicit HeapGraphContext(const HeapGraph
& hg
) : hg(hg
) {}
122 explicit HeapGraphContext(HeapGraph
&& hg
) : hg(std::move(hg
)) {}
123 ~HeapGraphContext() {}
125 bool isInvalid() const override
{
129 CLASSNAME_IS("HeapGraphContext")
130 DECLARE_RESOURCE_ALLOCATION(HeapGraphContext
)
132 // overriding ResourceData
133 const String
& o_getClassNameHook() const override
{ return classnameof(); }
135 std::vector
<CapturedNode
> cnodes
;
136 std::vector
<CapturedPtr
> cptrs
;
140 IMPLEMENT_RESOURCE_ALLOCATION(HeapGraphContext
)
144 using HeapGraphContextPtr
= req::ptr
<HeapGraphContext
>;
145 static HeapGraphContextPtr
get_valid_heapgraph_context_resource(
146 const Resource
& resource
,
147 const char* func_name
149 auto hgcontext
= dyn_cast_or_null
<HeapGraphContext
>(resource
);
150 if (hgcontext
== nullptr || hgcontext
->isInvalid()) {
152 "%s(): supplied resource is not a valid HeapGraph Context resource",
160 ///////////////////////////////////////////////////////////////////////////////
161 // TRAVERSAL FUNCTIONS
163 static std::array
<const StringData
*, 3> edge_kind_strs
{};
164 static const char* edge_kind_cstrs
[] = {
165 "Ptr:Counted", "Ptr:Ambiguous", "Ptr:Weak",
168 const StringData
* edgeKindName(HeapGraph::PtrKind kind
) {
169 auto s
= edge_kind_strs
[(int)kind
];
171 s
= makeStaticString(edge_kind_cstrs
[(int)kind
]);
172 edge_kind_strs
[(int)kind
] = s
;
175 static_assert(HeapGraph::NumPtrKinds
== 3, "");
176 static_assert(HeapGraph::Counted
== 0, "");
177 static_assert(HeapGraph::Ambiguous
== 1, "");
178 static_assert(HeapGraph::Weak
== 2, "");
181 // return metadata about this pointer, in the form of a CapturedPtr
182 CapturedPtr
getEdgeInfo(const HeapGraph
& g
, int ptr
) {
183 // Try to drill down and resolve the edge name
184 assertx(g
.ptrs
[ptr
].from
!= -1 && g
.ptrs
[ptr
].to
!= -1);
185 auto& edge
= g
.ptrs
[ptr
];
186 auto& from
= g
.nodes
[edge
.from
];
187 int prop_offset
= edge
.offset
;
189 auto from_hdr
= from
.h
;
191 // get the actual ObjectData*. This deals with object kinds that
192 // have data before the object: AFWH, Native, and Closure. Compute
193 // prop_offset relative to the inner ObjectData.
194 const ObjectData
* from_obj
{nullptr};
195 if (from_hdr
->kind() == HeaderKind::AsyncFuncFrame
) {
196 from_obj
= asyncFuncWH(from_hdr
);
197 prop_offset
= edge
.offset
- (uintptr_t(from_obj
) - uintptr_t(from_hdr
));
198 } else if (from_hdr
->kind() == HeaderKind::NativeData
) {
199 from_obj
= Native::obj(static_cast<const NativeNode
*>(from_hdr
));
200 prop_offset
= edge
.offset
- (uintptr_t(from_obj
) - uintptr_t(from_hdr
));
201 } else if (from_hdr
->kind() == HeaderKind::ClosureHdr
) {
202 from_obj
= closureObj(from_hdr
);
203 prop_offset
= edge
.offset
- (uintptr_t(from_obj
) - uintptr_t(from_hdr
));
204 } else if (isObjectKind(from_hdr
->kind())) {
205 from_obj
= static_cast<const ObjectData
*>(from_hdr
);
206 prop_offset
= edge
.offset
;
209 switch (from_hdr
->kind()) {
210 // Known generalized cases that don't really need pointer kind
211 case HeaderKind::Mixed
:
212 case HeaderKind::Dict
:
213 case HeaderKind::Keyset
: {
214 if (edge
.offset
>= sizeof(MixedArray
)) {
215 using Elm
= MixedArray::Elm
;
216 auto elm_offset
= edge
.offset
- sizeof(MixedArray
);
217 uint32_t index
= elm_offset
/ sizeof(Elm
);
218 if (index
< static_cast<const MixedArray
*>(from_hdr
)->iterLimit()) {
219 auto field
= elm_offset
- index
* sizeof(Elm
);
220 if (field
== Elm::keyOff()) {
221 return {CapturedPtr::Key
, index
};
223 if (field
== Elm::dataOff() + offsetof(TypedValue
, m_data
)) {
224 return {CapturedPtr::Value
, index
};
231 case HeaderKind::Packed
:
232 case HeaderKind::VecArray
: {
233 if (edge
.offset
>= sizeof(ArrayData
)) {
234 auto elm_offset
= edge
.offset
- sizeof(ArrayData
);
235 uint32_t index
= elm_offset
/ sizeof(TypedValue
);
236 if (index
< static_cast<const ArrayData
*>(from_hdr
)->size()) {
237 return {CapturedPtr::Value
, index
};
243 case HeaderKind::Apc
: {
244 if (edge
.offset
>= sizeof(APCLocalArray
)) {
245 auto elm_offset
= edge
.offset
- sizeof(APCLocalArray
);
246 uint32_t index
= elm_offset
/ sizeof(TypedValue
);
247 if (index
< static_cast<const APCLocalArray
*>(from_hdr
)->size()) {
248 return {CapturedPtr::Value
, index
};
254 case HeaderKind::Pair
: {
255 if (edge
.offset
>= c_Pair::dataOffset()) {
256 auto elm_offset
= edge
.offset
- c_Pair::dataOffset();
257 uint32_t index
= elm_offset
/ sizeof(TypedValue
);
259 return {CapturedPtr::Value
, index
};
265 case HeaderKind::AwaitAllWH
:
266 case HeaderKind::WaitHandle
:
267 case HeaderKind::Ref
:
270 // cases that have explicit pointer name
271 case HeaderKind::AsyncFuncFrame
:
272 case HeaderKind::ClosureHdr
:
273 case HeaderKind::Closure
:
274 // the class of a c_Closure describes the captured variables
275 case HeaderKind::NativeData
:
276 case HeaderKind::Object
: {
277 auto cls
= from_obj
->getVMClass();
278 FTRACE(5, "HG: Getting connection name for class {} at {}\n",
279 from_obj
->getClassName().data(), from_obj
);
280 if (prop_offset
>= sizeof(ObjectData
)) {
281 uint32_t index
= (prop_offset
- sizeof(ObjectData
)) /
283 if (index
< cls
->numDeclProperties()) {
284 return {CapturedPtr::Property
, index
};
287 // edge_offset > 0 && prop_offset < 0 means nativedata fields
292 case HeaderKind::NativeObject
:
293 case HeaderKind::AsyncFuncWH
:
294 case HeaderKind::Vector
:
295 case HeaderKind::ImmVector
:
296 case HeaderKind::Set
:
297 case HeaderKind::ImmSet
:
298 case HeaderKind::Map
:
299 case HeaderKind::ImmMap
:
300 case HeaderKind::Empty
:
301 case HeaderKind::Globals
:
302 case HeaderKind::String
:
303 case HeaderKind::Resource
:
304 case HeaderKind::BigMalloc
:
305 case HeaderKind::Cpp
:
306 case HeaderKind::SmallMalloc
:
307 case HeaderKind::Free
:
308 case HeaderKind::BigObj
:
309 case HeaderKind::Hole
:
310 case HeaderKind::Slab
:
311 // just provide raw prop_offset
316 return {CapturedPtr::Offset
, uint32_t(prop_offset
)};
319 void heapgraphCallback(Array fields
, const Variant
& callback
) {
321 auto params
= make_packed_array(fields
);
322 vm_call_user_func(callback
, params
);
325 void heapgraphCallback(Array fields
, Array fields2
, const Variant
& callback
) {
327 auto params
= make_packed_array(fields
, fields2
);
328 vm_call_user_func(callback
, params
);
331 static bool isStaticLocal(const HeapGraph::Node
& node
) {
332 return node
.tyindex
== type_scan::getIndexForScan
<rds::StaticLocalData
>() &&
333 type_scan::hasNonConservative();
336 static bool isStaticProp(const HeapGraph::Node
& node
) {
337 return node
.tyindex
== type_scan::getIndexForScan
<StaticPropData
>() &&
338 type_scan::hasNonConservative();
341 static const StringData
* header_name_strs
[NumHeaderKinds
];
343 Array
createPhpNode(HeapGraphContextPtr hgptr
, int index
) {
344 const auto& node
= hgptr
->hg
.nodes
[index
];
345 const auto& cnode
= hgptr
->cnodes
[index
];
347 const StringData
* kind_str
;
349 auto k
= int(cnode
.heap_object
.kind
);
350 kind_str
= header_name_strs
[k
];
352 kind_str
= makeStaticString(header_names
[k
]);
353 header_name_strs
[k
] = kind_str
;
356 kind_str
= s_Root
.get(); // fake HeaderKind "Root"
359 auto node_arr
= make_map_array(
360 s_index
, VarNR(index
),
361 s_kind
, VarNR(kind_str
),
362 s_size
, VarNR(int64_t(node
.size
))
364 if (type_scan::hasNonConservative()) {
365 auto ty
= node
.tyindex
;
366 if (ty
> type_scan::kIndexUnknownNoPtrs
) {
367 auto type
= type_scan::getName(ty
);
369 make_tv
<KindOfPersistentString
>(makeStaticString(type
)));
373 if (auto cls
= cnode
.heap_object
.cls
) {
374 node_arr
.set(s_class
, make_tv
<KindOfPersistentString
>(cls
->name()));
376 } else if (isStaticLocal(node
)) {
377 node_arr
.set(s_local
,
378 make_tv
<KindOfPersistentString
>(cnode
.static_local
.name
));
379 auto func_id
= cnode
.static_local
.funcId
;
380 if (func_id
!= InvalidFuncId
) {
381 auto func
= Func::fromFuncId(cnode
.static_local
.funcId
);
382 node_arr
.set(s_func
, make_tv
<KindOfPersistentString
>(func
->name()));
383 if (auto cls
= func
->cls()) {
384 node_arr
.set(s_class
, make_tv
<KindOfPersistentString
>(cls
->name()));
387 } else if (isStaticProp(node
)) {
388 if (auto cls
= cnode
.sprop_cache
.cls
) {
389 auto& sprop
= cls
->staticProperties()[cnode
.sprop_cache
.slot
];
390 node_arr
.set(s_class
, make_tv
<KindOfPersistentString
>(cls
->name()));
391 node_arr
.set(s_prop
, make_tv
<KindOfPersistentString
>(sprop
.name
));
397 Array
createPhpEdge(HeapGraphContextPtr hgptr
, int index
) {
398 const auto& ptr
= hgptr
->hg
.ptrs
[index
];
399 const auto& cptr
= hgptr
->cptrs
[index
];
400 const auto& cfrom
= hgptr
->cnodes
[ptr
.from
];
402 auto ptr_arr
= make_map_array(
403 s_index
, VarNR(index
),
404 s_kind
, VarNR(edgeKindName(ptr
.ptr_kind
)),
405 s_from
, VarNR(ptr
.from
),
408 switch (cptr
.index_kind
) {
409 case CapturedPtr::Key
:
410 ptr_arr
.set(s_key
, make_tv
<KindOfInt64
>(cptr
.index
));
412 case CapturedPtr::Value
:
413 ptr_arr
.set(s_value
, make_tv
<KindOfInt64
>(cptr
.index
));
415 case CapturedPtr::Property
: {
416 auto& prop
= cfrom
.heap_object
.cls
->declProperties()[cptr
.index
];
417 ptr_arr
.set(s_prop
, make_tv
<KindOfPersistentString
>(prop
.name
));
420 case CapturedPtr::Offset
:
421 if (cptr
.index
) ptr_arr
.set(s_offset
, make_tv
<KindOfInt64
>(cptr
.index
));
428 std::vector
<int> toBoundIntVector(const Array
& arr
, int64_t max
) {
429 std::vector
<int> result
;
430 result
.reserve(arr
.size());
431 for (ArrayIter
iter(arr
); iter
; ++iter
) {
432 auto index
= iter
.second().toInt64(); // Cannot re-enter.
433 if (index
< 0 || index
>= max
) {
437 result
.push_back(index
);
442 ///////////////////////////////////////////////////////////////////////////////
445 Resource
HHVM_FUNCTION(heapgraph_create
, void) {
446 HeapGraph hg
= makeHeapGraph();
447 std::vector
<CapturedNode
> cnodes
;
448 std::vector
<CapturedPtr
> cptrs
;
450 // Copy edges into captured edges
451 // Capturing edges first because after capturing nodes we nullify the header
452 cptrs
.reserve(hg
.ptrs
.size());
453 for (int i
= 0; i
< hg
.ptrs
.size(); ++i
) {
454 auto new_ptr
= getEdgeInfo(hg
, i
); // edge name
455 cptrs
.push_back(new_ptr
);
458 // Copy useful information from heap into cnodes
459 cnodes
.resize(hg
.nodes
.size());
460 for (size_t i
= 0, n
= hg
.nodes
.size(); i
< n
; ++i
) {
461 auto& node
= hg
.nodes
[i
];
462 auto& cnode
= cnodes
[i
];
464 auto obj
= innerObj(node
.h
);
465 cnode
.heap_object
.kind
= node
.h
->kind();
466 cnode
.heap_object
.cls
= obj
? obj
->getVMClass() : nullptr;
467 } else if (isStaticLocal(node
)) {
468 rds::Handle handle
= rds::ptrToHandle
<rds::Mode::Any
>(node
.ptr
);
469 auto sym
= rds::reverseLink(handle
);
471 cnode
.static_local
= boost::get
<rds::StaticLocal
>(sym
.value());
473 cnode
.static_local
= {InvalidFuncId
, nullptr};
475 } else if (isStaticProp(node
)) {
476 rds::Handle handle
= rds::ptrToHandle
<rds::Mode::Any
>(node
.ptr
);
477 auto sym
= rds::reverseLink(handle
);
479 cnode
.sprop_cache
= boost::get
<rds::SPropCache
>(sym
.value());
481 cnode
.sprop_cache
= {nullptr, 0};
485 // Nullify the pointers to be safe since this is a captured heap
489 auto hgcontext
= req::make
<HeapGraphContext
>(std::move(hg
));
490 std::swap(hgcontext
->cnodes
, cnodes
);
491 std::swap(hgcontext
->cptrs
, cptrs
);
492 return Resource(hgcontext
);
495 void HHVM_FUNCTION(heapgraph_foreach_node
,
496 const Resource
& resource
,
497 const Variant
& callback
499 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
500 if (!hgptr
|| callback
.isNull()) return;
501 for (int i
= 0; i
< hgptr
->hg
.nodes
.size(); i
++) {
502 auto phpnode
= createPhpNode(hgptr
, i
);
503 heapgraphCallback(phpnode
, callback
);
507 void HHVM_FUNCTION(heapgraph_foreach_edge
,
508 const Resource
& resource
,
509 const Variant
& callback
511 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
512 if (!hgptr
|| callback
.isNull()) return;
513 for (int i
= 0; i
< hgptr
->hg
.ptrs
.size(); i
++) {
514 auto phpedge
= createPhpEdge(hgptr
, i
);
515 heapgraphCallback(phpedge
, callback
);
519 void HHVM_FUNCTION(heapgraph_foreach_root
,
520 const Resource
& resource
,
521 const Variant
& callback
523 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
524 if (!hgptr
|| callback
.isNull()) return;
525 for (int i
= 0, n
= hgptr
->hg
.root_ptrs
.size(); i
< n
; ++i
) {
526 auto phpedge
= createPhpEdge(hgptr
, hgptr
->hg
.root_ptrs
[i
]);
527 heapgraphCallback(phpedge
, callback
);
531 void HHVM_FUNCTION(heapgraph_foreach_root_node
,
532 const Resource
& resource
,
533 const Variant
& callback
535 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
536 if (!hgptr
|| callback
.isNull()) return;
537 for (auto n
: hgptr
->hg
.root_nodes
) {
538 auto phpnode
= createPhpNode(hgptr
, n
);
539 heapgraphCallback(phpnode
, callback
);
543 void HHVM_FUNCTION(heapgraph_dfs_nodes
,
544 const Resource
& resource
,
545 const Array
& roots_arr
,
546 const Array
& skips_arr
,
547 const Variant
& callback
549 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
550 if (!hgptr
|| callback
.isNull()) return;
551 auto max
= hgptr
->hg
.nodes
.size();
552 auto roots
= toBoundIntVector(roots_arr
, max
);
553 auto skips
= toBoundIntVector(skips_arr
, max
);
554 dfs_nodes(hgptr
->hg
, roots
, skips
, [&](int n
) {
555 auto phpnode
= createPhpNode(hgptr
, n
);
556 heapgraphCallback(phpnode
, callback
);
560 void HHVM_FUNCTION(heapgraph_dfs_edges
,
561 const Resource
& resource
,
562 const Array
& roots_arr
,
563 const Array
& skips_arr
,
564 const Variant
& callback
566 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
567 if (!hgptr
|| callback
.isNull()) return;
568 auto max
= hgptr
->hg
.ptrs
.size();
569 auto roots
= toBoundIntVector(roots_arr
, max
);
570 auto skips
= toBoundIntVector(skips_arr
, max
);
571 dfs_ptrs(hgptr
->hg
, roots
, skips
, [&](int n
, int p
) {
572 auto phpnode
= createPhpNode(hgptr
, n
);
573 auto phpedge
= createPhpEdge(hgptr
, p
);
574 heapgraphCallback(phpedge
, phpnode
, callback
);
578 Array
HHVM_FUNCTION(heapgraph_edge
, const Resource
& resource
, int64_t index
) {
579 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
580 if (!hgptr
) return empty_array();
581 if (size_t(index
) >= hgptr
->hg
.ptrs
.size()) return empty_array();
582 return createPhpEdge(hgptr
, index
);
585 Array
HHVM_FUNCTION(heapgraph_node
, const Resource
& resource
, int64_t index
) {
586 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
587 if (!hgptr
) return empty_array();
588 if (size_t(index
) >= hgptr
->hg
.nodes
.size()) return empty_array();
589 return createPhpNode(hgptr
, index
);
592 Array
HHVM_FUNCTION(heapgraph_node_out_edges
,
593 const Resource
& resource
,
596 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
597 if (!hgptr
) return empty_array();
598 if (size_t(index
) >= hgptr
->hg
.nodes
.size()) return empty_array();
600 hgptr
->hg
.eachOutPtr(index
, [&](int) { num_edges
++; });
601 PackedArrayInit
result(num_edges
);
602 hgptr
->hg
.eachOutPtr(index
, [&](int ptr
) {
603 result
.append(createPhpEdge(hgptr
, ptr
));
605 return result
.toArray();
608 Array
HHVM_FUNCTION(heapgraph_node_in_edges
,
609 const Resource
& resource
,
612 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
613 if (!hgptr
) return empty_array();
614 if (size_t(index
) >= hgptr
->hg
.nodes
.size()) return empty_array();
616 hgptr
->hg
.eachInPtr(index
, [&](int) { num_edges
++; });
617 PackedArrayInit
result(num_edges
);
618 hgptr
->hg
.eachInPtr(index
, [&](int ptr
) {
619 result
.append(createPhpEdge(hgptr
, ptr
));
621 return result
.toArray();
624 Array
HHVM_FUNCTION(heapgraph_stats
, const Resource
& resource
) {
625 auto hgptr
= get_valid_heapgraph_context_resource(resource
, __FUNCTION__
);
626 if (!hgptr
) return empty_array();
627 auto result
= make_map_array(
628 s_nodes
, VarNR(int64_t(hgptr
->hg
.nodes
.size())),
629 s_edges
, VarNR(int64_t(hgptr
->hg
.ptrs
.size())),
630 s_roots
, VarNR(int64_t(hgptr
->hg
.root_ptrs
.size())),
631 s_root_nodes
, VarNR(int64_t(hgptr
->hg
.root_nodes
.size())),
632 s_exact
, VarNR(type_scan::hasNonConservative() ? 1 : 0)
637 ///////////////////////////////////////////////////////////////////////////////
642 struct heapgraphExtension final
: Extension
{
643 heapgraphExtension() : Extension("heapgraph", "1.0") { }
645 void moduleInit() override
{
646 HHVM_FALIAS(HH
\\heapgraph_create
, heapgraph_create
);
647 HHVM_FALIAS(HH
\\heapgraph_stats
, heapgraph_stats
);
648 HHVM_FALIAS(HH
\\heapgraph_foreach_node
, heapgraph_foreach_node
);
649 HHVM_FALIAS(HH
\\heapgraph_foreach_edge
, heapgraph_foreach_edge
);
650 HHVM_FALIAS(HH
\\heapgraph_foreach_root
, heapgraph_foreach_root
);
651 HHVM_FALIAS(HH
\\heapgraph_foreach_root_node
, heapgraph_foreach_root_node
);
652 HHVM_FALIAS(HH
\\heapgraph_edge
, heapgraph_edge
);
653 HHVM_FALIAS(HH
\\heapgraph_node
, heapgraph_node
);
654 HHVM_FALIAS(HH
\\heapgraph_node_out_edges
, heapgraph_node_out_edges
);
655 HHVM_FALIAS(HH
\\heapgraph_node_in_edges
, heapgraph_node_in_edges
);
656 HHVM_FALIAS(HH
\\heapgraph_dfs_nodes
, heapgraph_dfs_nodes
);
657 HHVM_FALIAS(HH
\\heapgraph_dfs_edges
, heapgraph_dfs_edges
);
661 } s_heapgraph_extension
;
664 ///////////////////////////////////////////////////////////////////////////////