<lambda>null1 package shark
2
3 import com.github.ajalt.clikt.core.Abort
4 import com.github.ajalt.clikt.core.CliktCommand
5 import com.github.ajalt.clikt.output.TermUi
6 import com.github.ajalt.clikt.parameters.arguments.argument
7 import com.github.ajalt.clikt.parameters.arguments.optional
8 import com.github.ajalt.clikt.parameters.types.file
9 import java.io.File
10 import java.io.PrintWriter
11 import java.io.StringWriter
12 import java.lang.ref.PhantomReference
13 import java.lang.ref.SoftReference
14 import java.lang.ref.WeakReference
15 import java.util.regex.Pattern
16 import jline.console.ConsoleReader
17 import org.neo4j.configuration.GraphDatabaseSettings
18 import org.neo4j.configuration.connectors.BoltConnector
19 import org.neo4j.configuration.helpers.SocketAddress
20 import org.neo4j.dbms.api.DatabaseManagementServiceBuilder
21 import org.neo4j.graphdb.Entity
22 import org.neo4j.graphdb.GraphDatabaseService
23 import org.neo4j.graphdb.Path
24 import org.neo4j.graphdb.Relationship
25 import org.neo4j.graphdb.Transaction
26 import org.neo4j.kernel.api.procedure.GlobalProcedures
27 import org.neo4j.kernel.internal.GraphDatabaseAPI
28 import org.neo4j.logging.Level
29 import org.neo4j.procedure.UserFunction
30 import shark.HeapObject.HeapClass
31 import shark.HeapObject.HeapInstance
32 import shark.HeapObject.HeapObjectArray
33 import shark.HeapObject.HeapPrimitiveArray
34 import shark.HprofHeapGraph.Companion.openHeapGraph
35 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.BooleanArrayDump
36 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ByteArrayDump
37 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.CharArrayDump
38 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.DoubleArrayDump
39 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.FloatArrayDump
40 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.IntArrayDump
41 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump
42 import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump
43 import shark.LeakTraceObject.LeakingStatus.LEAKING
44 import shark.LeakTraceObject.LeakingStatus.NOT_LEAKING
45 import shark.LeakTraceObject.LeakingStatus.UNKNOWN
46 import shark.SharkCliCommand.Companion.echo
47 import shark.SharkCliCommand.Companion.retrieveHeapDumpFile
48 import shark.SharkCliCommand.Companion.sharkCliParams
49 import shark.ValueHolder.BooleanHolder
50 import shark.ValueHolder.ByteHolder
51 import shark.ValueHolder.CharHolder
52 import shark.ValueHolder.DoubleHolder
53 import shark.ValueHolder.FloatHolder
54 import shark.ValueHolder.IntHolder
55 import shark.ValueHolder.LongHolder
56 import shark.ValueHolder.ReferenceHolder
57 import shark.ValueHolder.ShortHolder
58
59
60 /**
61 * Example commands:
62 *
63 * MATCH (roots: GcRoots)
64 * RETURN roots
65 *
66 * MATCH (activity: Instance) -[:CLASS|SUPER*1..]-> (c:Class {className: "android.app.Activity"})
67 * RETURN activity
68 *
69 * MATCH (activity: Instance) -[:CLASS|SUPER*1..]-> (c:Class {className: "android.app.Activity"})
70 * WHERE "android.app.Activity.mDestroyed = true" in activity.fields
71 * RETURN activity
72 *
73 * MATCH (roots: GcRoots)
74 * MATCH (activity: Instance) -[:CLASS|SUPER*1..]->(c:Class {className: "android.app.Activity"})
75 * WHERE "android.app.Activity.mDestroyed = true" in activity.fields
76 * RETURN shortestPath((roots)-[:ROOT|REF*]->(activity))
77 */
78 @SuppressWarnings("MaxLineLength", "LongMethod")
79 class Neo4JCommand : CliktCommand(
80 name = "neo4j",
81 help = "Convert heap dump to Neo4j database"
82 ) {
83
84 private val optionalDbFolder by argument("NEO4J_DATABASE_DIRECTORY").file(
85 exists = true,
86 fileOkay = false,
87 folderOkay = true,
88 writable = true
89 ).optional()
90
91 override fun run() {
92 val params = context.sharkCliParams
93 val heapDumpFile = retrieveHeapDumpFile(params)
94
95 val dbFolder = optionalDbFolder ?: heapDumpFile.parentFile
96
97 dump(heapDumpFile, dbFolder, params.obfuscationMappingPath)
98 }
99
100 companion object {
101 val REFERENCE = "REF"
102 val WEAK_REFERENCE = "WEAK_REF"
103 val SOFT_REFERENCE = "SOFT_REF"
104 val PHANTOM_REFERENCE = "PHANTOM_REF"
105
106 fun CliktCommand.dump(
107 heapDumpFile: File,
108 dbParentFolder: File,
109 proguardMappingFile: File?
110 ) {
111 val proguardMapping = proguardMappingFile?.let {
112 ProguardMappingReader(it.inputStream()).readProguardMapping()
113 }
114
115 val name = heapDumpFile.name.substringBeforeLast(".hprof")
116 val dbFolder = File(dbParentFolder, name)
117
118 if (dbFolder.exists()) {
119 val continueImport = TermUi.confirm(
120 "Directory $dbFolder already exists, delete it and continue?",
121 default = true,
122 abort = true
123 ) ?: false
124
125 if (!continueImport) {
126 throw Abort()
127 }
128 echo("Deleting $dbFolder")
129 dbFolder.deleteRecursively()
130 }
131
132 // Ideally we'd get a random free port. We could do this by using
133 // `new SocketAddress("localhost", 0)` but we wouldn't be able to retrieve the port via Neo4j
134 // means afterwards (it would indicate 0 as ephemeral port)
135 val boltListenPort = 2929
136
137 echo("Creating db in $dbFolder")
138 // Note: we're creating an embedded neo4j rather than connecting to a real neo4j instance,
139 // mostly out of convenience (ie this is a demo that works without installing anything)
140 val managementService =
141 DatabaseManagementServiceBuilder(dbFolder.toPath().normalize())
142 .setConfig(BoltConnector.enabled, true)
143 .setConfig(BoltConnector.listen_address, SocketAddress("localhost", boltListenPort))
144 .setConfig(GraphDatabaseSettings.store_internal_log_level, Level.DEBUG)
145 .build()
146 val dbService = managementService.database("neo4j")
147 val api = dbService as GraphDatabaseAPI
148 val registry = api.dependencyResolver.resolveDependency(GlobalProcedures::class.java)
149 registry.registerFunction(FindLeakPaths::class.java)
150
151 echo("Done with creating empty db, now importing heap dump")
152 heapDumpFile.openHeapGraph(proguardMapping).use { graph ->
153
154 val total = graph.objectCount
155
156
157 dbService.executeTransactionally("create constraint for (object:Object) require object.objectId is unique")
158 dbService.executeTransactionally("create constraint for (class:Class) require class.objectId is unique")
159 dbService.executeTransactionally("create constraint for (instance:Instance) require instance.objectId is unique")
160 dbService.executeTransactionally("create constraint for (array:ObjectArray) require array.objectId is unique")
161 dbService.executeTransactionally("create constraint for (array:PrimitiveArray) require array.objectId is unique")
162 // TODO Split in several transactions when reaching a predefined threshold.
163
164 dumpNodes(dbService, graph, total)
165 dumpClassHierarchy(dbService, graph, total)
166 dumpEdges(dbService, graph, total)
167 dumpLabels(dbService, graph, total)
168 dumpGcRoots(dbService, graph)
169 }
170
171 echo("Retrieving server bolt port...")
172
173 // TODO Unclear why we need to query the port again?
174 val boltPort = dbService.executeTransactionally(
175 "CALL dbms.listConfig() yield name, value " +
176 "WHERE name = 'dbms.connector.bolt.listen_address' " +
177 "RETURN value", mapOf()
178 ) { result ->
179 val listenAddress = result.next()["value"] as String
180 val pattern = Pattern.compile("(?:\\w+:)?(\\d+)")
181 val matcher = pattern.matcher(listenAddress)
182 if (!matcher.matches()) {
183 error("Could not extract bolt port from [$listenAddress]")
184 }
185 matcher.toMatchResult().group(1)
186 }
187
188 val browserUrl = "http://browser.graphapp.io/?dbms=bolt://localhost:$boltPort"
189 echo("Opening: $browserUrl")
190 Runtime.getRuntime().exec("open $browserUrl")
191 ConsoleReader().readLine("Press ENTER to shut down Neo4j server")
192 echo("Shutting down...")
193 managementService.shutdown()
194 }
195
196 private fun CliktCommand.dumpGcRoots(
197 dbService: GraphDatabaseService,
198 graph: CloseableHeapGraph
199 ) {
200 val gcRootsTx = dbService.beginTx()
201 echo("Progress gc roots: 0%")
202 var lastPct = 0
203 val gcRootTotal = graph.gcRoots.size
204
205 // A root for all gc roots that makes it easy to query starting from that single root.
206 gcRootsTx.execute("create (:GcRoots {name:\"GC roots\", leakingStatus:\"${NOT_LEAKING.name}\"})")
207
208 graph.gcRoots.forEachIndexed { index, gcRoot ->
209 val pct = ((index * 10f) / gcRootTotal).toInt()
210 if (pct != lastPct) {
211 lastPct = pct
212 echo("Progress gc roots: ${pct * 10}%")
213 }
214 gcRootsTx.execute(
215 "match (roots:GcRoots), (object:Object{objectId:\$objectId}) create (roots)-[:ROOT]->(:GcRoot {type:\$type})-[:ROOT]->(object)",
216 mapOf(
217 "objectId" to gcRoot.id,
218 "type" to gcRoot::class.java.simpleName
219 )
220 )
221 }
222 echo("Progress gc roots: 100%, committing transaction")
223 gcRootsTx.commit()
224 }
225
226 private fun CliktCommand.dumpLabels(
227 dbService: GraphDatabaseService,
228 graph: CloseableHeapGraph,
229 total: Int
230 ) {
231 val labelsTx = dbService.beginTx()
232 echo("Progress labels: 0%")
233 var lastPct = 0
234 val inspectors = AndroidObjectInspectors.appDefaults
235 val leakFilters = ObjectInspectors.jdkLeakingObjectFilters
236
237 graph.objects.forEachIndexed { index, heapObject ->
238 val pct = ((index * 10f) / total).toInt()
239 if (pct != lastPct) {
240 lastPct = pct
241 echo("Progress labels: ${pct * 10}%")
242 }
243
244 val leaked = leakFilters.any { filter ->
245 filter.isLeakingObject(heapObject)
246 }
247
248 val reporter = ObjectReporter(heapObject)
249 inspectors.forEach { inspector ->
250 inspector.inspect(reporter)
251 }
252
253 // Cribbed from shark.HeapAnalyzer.resolveStatus
254 var status = UNKNOWN
255 var reason = ""
256 if (reporter.notLeakingReasons.isNotEmpty()) {
257 status = NOT_LEAKING
258 reason = reporter.notLeakingReasons.joinToString(" and ")
259 }
260 val leakingReasons = reporter.leakingReasons
261 if (leakingReasons.isNotEmpty()) {
262 val winReasons = leakingReasons.joinToString(" and ")
263 // Conflict
264 if (status == NOT_LEAKING) {
265 reason += ". Conflicts with $winReasons"
266 } else {
267 status = LEAKING
268 reason = winReasons
269 }
270 }
271
272 labelsTx.execute(
273 "match (node:Object{objectId:\$objectId})" +
274 " set node.leakingStatus = \$leakingStatus, node.leakingStatusReason = \$leakingStatusReason",
275 mapOf(
276 "objectId" to heapObject.objectId,
277 "leakingStatus" to status.name,
278 "leakingStatusReason" to reason
279 )
280 )
281
282 if (reporter.labels.isNotEmpty()) {
283 labelsTx.execute(
284 "match (node:Object{objectId:\$objectId})" +
285 " set node.labels = \$labels",
286 mapOf(
287 "objectId" to heapObject.objectId,
288 "labels" to reporter.labels,
289 )
290 )
291 }
292
293 if (leaked) {
294 labelsTx.execute(
295 "match (node:Object{objectId:\$objectId})" +
296 " set node.leaked = true",
297 mapOf(
298 "objectId" to heapObject.objectId,
299 )
300 )
301 }
302 }
303 echo("Progress labels: 100%, committing transaction")
304 labelsTx.commit()
305 }
306
307 private fun CliktCommand.dumpEdges(
308 dbService: GraphDatabaseService,
309 graph: CloseableHeapGraph,
310 total: Int
311 ) {
312 val edgeTx = dbService.beginTx()
313 echo("Progress edges: 0%")
314 var lastPct = 0
315 graph.objects.forEachIndexed { index, heapObject ->
316 val pct = ((index * 10f) / total).toInt()
317 if (pct != lastPct) {
318 lastPct = pct
319 echo("Progress edges: ${pct * 10}%")
320 }
321 when (heapObject) {
322 is HeapClass -> {
323 val fields = heapObject.readStaticFields().mapNotNull { field ->
324 if (field.value.isNonNullReference) {
325 mapOf(
326 "targetObjectId" to field.value.asObjectId!!,
327 "name" to field.name
328 )
329 } else {
330 null
331 }
332 }.toList()
333
334 edgeTx.execute(
335 "unwind \$fields as field" +
336 " match (source:Object{objectId:\$sourceObjectId}), (target:Object{objectId:field.targetObjectId})" +
337 " create (source)-[:$REFERENCE {name:field.name}]->(target)", mapOf(
338 "sourceObjectId" to heapObject.objectId,
339 "fields" to fields
340 )
341 )
342
343 val primitiveAndNullFields = heapObject.readStaticFields().mapNotNull { field ->
344 if (!field.value.isNonNullReference) {
345 "${field.name}: ${field.value.heapValueAsString()}"
346 } else {
347 null
348 }
349 }.toList()
350
351 edgeTx.execute(
352 "match (node:Object{objectId:\$objectId})" +
353 " set node.staticFields = \$values",
354 mapOf(
355 "objectId" to heapObject.objectId,
356 "values" to primitiveAndNullFields
357 )
358 )
359 }
360 is HeapInstance -> {
361 val fields = heapObject.readFields().mapNotNull { field ->
362 if (field.value.isNonNullReference) {
363 mapOf(
364 "targetObjectId" to field.value.asObjectId!!,
365 "name" to "${field.declaringClass.name}.${field.name}"
366 )
367 } else {
368 null
369 }
370 }.toList()
371
372 val (updatedFields, referentField, refType) = when {
373 heapObject instanceOf WeakReference::class -> {
374 val referentField = heapObject["java.lang.ref.Reference", "referent"]
375 Triple(
376 fields.filter { it["name"] != "java.lang.ref.Reference.referent" },
377 referentField,
378 WEAK_REFERENCE
379 )
380 }
381 heapObject instanceOf SoftReference::class -> {
382 val referentField = heapObject["java.lang.ref.Reference", "referent"]
383 Triple(
384 fields.filter { it["name"] != "java.lang.ref.Reference.referent" },
385 referentField,
386 SOFT_REFERENCE
387 )
388 }
389 heapObject instanceOf PhantomReference::class -> {
390 val referentField = heapObject["java.lang.ref.Reference", "referent"]
391 Triple(
392 fields.filter { it["name"] != "java.lang.ref.Reference.referent" },
393 referentField,
394 PHANTOM_REFERENCE
395 )
396 }
397 else -> Triple(fields, null, null)
398 }
399
400 edgeTx.execute(
401 "unwind \$fields as field" +
402 " match (source:Object{objectId:\$sourceObjectId}), (target:Object{objectId:field.targetObjectId})" +
403 " create (source)-[:$REFERENCE {name:field.name}]->(target)", mapOf(
404 "sourceObjectId" to heapObject.objectId,
405 "fields" to updatedFields
406 )
407 )
408
409 if (referentField != null) {
410 edgeTx.execute(
411 "match (source:Object{objectId:\$sourceObjectId}), (target:Object{objectId:\$targetObjectId})" +
412 " create (source)-[:$refType {name:\"java.lang.ref.Reference.referent\"}]->(target)",
413 mapOf(
414 "sourceObjectId" to heapObject.objectId,
415 "targetObjectId" to referentField.value.asObjectId!!,
416 )
417 )
418 }
419
420 val primitiveAndNullFields = heapObject.readFields().mapNotNull { field ->
421 if (!field.value.isNonNullReference) {
422 "${field.declaringClass.name}.${field.name} = ${field.value.heapValueAsString()}"
423 } else {
424 null
425 }
426 }.toList()
427
428 edgeTx.execute(
429 "match (node:Object{objectId:\$objectId})" +
430 " set node.fields = \$values",
431 mapOf(
432 "objectId" to heapObject.objectId,
433 "values" to primitiveAndNullFields
434 )
435 )
436 }
437 is HeapObjectArray -> {
438 // TODO Add null values somehow?
439 val elements = heapObject.readRecord().elementIds.mapIndexed { arrayIndex, objectId ->
440 if (objectId != ValueHolder.NULL_REFERENCE) {
441 mapOf(
442 "targetObjectId" to objectId,
443 "name" to "[$arrayIndex]"
444 )
445 } else {
446 null
447 }
448 }.filterNotNull().toList()
449
450 edgeTx.execute(
451 "unwind \$elements as element" +
452 " match (source:Object{objectId:\$sourceObjectId}), (target:Object{objectId:element.targetObjectId})" +
453 " create (source)-[:$REFERENCE {name:element.name}]->(target)", mapOf(
454 "sourceObjectId" to heapObject.objectId,
455 "elements" to elements
456 )
457 )
458 }
459 is HeapPrimitiveArray -> {
460 when (val record = heapObject.readRecord()) {
461 is BooleanArrayDump -> {
462 edgeTx.execute(
463 "match (node:Object{objectId:\$objectId})" +
464 " set node.values = \$values",
465 mapOf(
466 "objectId" to heapObject.objectId,
467 "values" to record.array.joinToString()
468 )
469 )
470 }
471 is ByteArrayDump -> {
472 edgeTx.execute(
473 "match (node:Object{objectId:\$objectId})" +
474 " set node.values = \$values",
475 mapOf(
476 "objectId" to heapObject.objectId,
477 "values" to record.array.joinToString()
478 )
479 )
480 }
481 is CharArrayDump -> {
482 edgeTx.execute(
483 "match (node:Object{objectId:\$objectId})" +
484 " set node.values = \$values",
485 mapOf(
486 "objectId" to heapObject.objectId,
487 "values" to record.array.joinToString()
488 )
489 )
490 }
491 is DoubleArrayDump -> {
492 edgeTx.execute(
493 "match (node:Object{objectId:\$objectId})" +
494 " set node.values = \$values",
495 mapOf(
496 "objectId" to heapObject.objectId,
497 "values" to record.array.joinToString()
498 )
499 )
500 }
501 is FloatArrayDump -> {
502 edgeTx.execute(
503 "match (node:Object{objectId:\$objectId})" +
504 " set node.values = \$values",
505 mapOf(
506 "objectId" to heapObject.objectId,
507 "values" to record.array.joinToString()
508 )
509 )
510 }
511 is IntArrayDump -> {
512 edgeTx.execute(
513 "match (node:Object{objectId:\$objectId})" +
514 " set node.values = \$values",
515 mapOf(
516 "objectId" to heapObject.objectId,
517 "values" to record.array.joinToString()
518 )
519 )
520 }
521 is LongArrayDump -> {
522 edgeTx.execute(
523 "match (node:Object{objectId:\$objectId})" +
524 " set node.values = \$values",
525 mapOf(
526 "objectId" to heapObject.objectId,
527 "values" to record.array.joinToString()
528 )
529 )
530 }
531 is ShortArrayDump -> {
532 edgeTx.execute(
533 "match (node:Object{objectId:\$objectId})" +
534 " set node.values = \$values",
535 mapOf(
536 "objectId" to heapObject.objectId,
537 "values" to record.array.joinToString()
538 )
539 )
540 }
541 }
542 }
543 }
544 }
545 echo("Progress edges: 100%, committing transaction")
546 edgeTx.commit()
547 }
548
549 private fun CliktCommand.dumpNodes(
550 dbService: GraphDatabaseService,
551 graph: CloseableHeapGraph,
552 total: Int,
553 ) {
554 var lastPct = 0
555 val nodeTx = dbService.beginTx()
556 echo("Progress nodes: 0%")
557 graph.objects.forEachIndexed { index, heapObject ->
558 val pct = ((index * 10f) / total).toInt()
559 if (pct != lastPct) {
560 lastPct = pct
561 echo("Progress nodes: ${pct * 10}%")
562 }
563 when (heapObject) {
564 is HeapClass -> {
565 heapObject.readStaticFields().forEach { field ->
566 field.name
567 field.value
568 }
569 nodeTx.execute(
570 "create (:Object :Class {objectType: 'Class', name:\$name, className:\$className, objectId:\$objectId})",
571 mapOf(
572 "name" to heapObject.simpleName,
573 "className" to heapObject.name,
574 "objectId" to heapObject.objectId
575 )
576 )
577 }
578 is HeapInstance -> {
579 nodeTx.execute(
580 "create (:Object :Instance {objectType: 'Instance', name:\$name, className:\$className, objectId:\$objectId})",
581 mapOf(
582 "name" to "${heapObject.instanceClassSimpleName}@" + (heapObject.hexIdentityHashCode
583 ?: heapObject.positiveObjectId),
584 "className" to heapObject.instanceClassName,
585 "objectId" to heapObject.objectId,
586 )
587 )
588 }
589 is HeapObjectArray -> {
590 nodeTx.execute(
591 "create (:Object :ObjectArray {objectType: 'ObjectArray', name:\$name," +
592 " className:\$className, objectId:\$objectId})",
593 mapOf(
594 "name" to "${heapObject.arrayClassSimpleName}[]@${heapObject.positiveObjectId}",
595 "className" to heapObject.arrayClassName,
596 "objectId" to heapObject.objectId,
597 )
598 )
599 }
600 is HeapPrimitiveArray -> {
601 nodeTx.execute(
602 "create (:Object :PrimitiveArray {objectType: 'PrimitiveArray', name:\$name," +
603 " className:\$className, objectId:\$objectId})",
604 mapOf(
605 "name" to "${heapObject.arrayClassName}@${heapObject.positiveObjectId}",
606 "className" to heapObject.arrayClassName,
607 "objectId" to heapObject.objectId
608 )
609 )
610 }
611 }
612 }
613 echo("Progress nodes: 100%, committing transaction")
614 nodeTx.commit()
615 }
616
617 private fun CliktCommand.dumpClassHierarchy(
618 dbService: GraphDatabaseService,
619 graph: CloseableHeapGraph,
620 total: Int
621 ) {
622 val classTx = dbService.beginTx()
623 echo("Progress class hierarchy: 0%")
624 var lastPct = 0
625 graph.objects.forEachIndexed { index, heapObject ->
626 val pct = ((index * 10f) / total).toInt()
627 if (pct != lastPct) {
628 lastPct = pct
629 echo("Progress class hierarchy: ${pct * 10}%")
630 }
631 when (heapObject) {
632 is HeapClass -> {
633 heapObject.superclass?.let { superclass ->
634 classTx.execute(
635 "match (superclass:Class{objectId:\$superclassObjectId}) ," +
636 " (class:Class {objectId:\$objectId}) create (class) -[:SUPER]->(superclass)",
637 mapOf(
638 "objectId" to heapObject.objectId,
639 "superclassObjectId" to superclass.objectId
640 )
641 )
642 }
643 }
644 is HeapInstance -> {
645 classTx.execute(
646 "match (class:Class{objectId:\$classObjectId}) ," +
647 " (instance:Instance {objectId:\$objectId}) create (instance) -[:CLASS]->(class)",
648 mapOf(
649 "objectId" to heapObject.objectId,
650 "classObjectId" to heapObject.instanceClassId
651 )
652 )
653 }
654 is HeapObjectArray -> {
655 classTx.execute(
656 "match (class:Class{objectId:\$classObjectId}) ," +
657 " (array:ObjectArray {objectId:\$objectId}) create (array) -[:CLASS]->(class)",
658 mapOf(
659 "objectId" to heapObject.objectId,
660 "classObjectId" to heapObject.arrayClassId
661 )
662 )
663 }
664 }
665 }
666 echo("Progress class hierarchy: 100%, committing transaction")
667 classTx.commit()
668 }
669
670 fun HeapValue.heapValueAsString(): String {
671 return when (val heapValue = holder) {
672 is ReferenceHolder -> {
673 if (isNullReference) {
674 "null"
675 } else {
676 error("should not happen")
677 }
678 }
679 is BooleanHolder -> heapValue.value.toString()
680 is CharHolder -> heapValue.value.toString()
681 is FloatHolder -> heapValue.value.toString()
682 is DoubleHolder -> heapValue.value.toString()
683 is ByteHolder -> heapValue.value.toString()
684 is ShortHolder -> heapValue.value.toString()
685 is IntHolder -> heapValue.value.toString()
686 is LongHolder -> heapValue.value.toString()
687 }
688 }
689 }
690 }
691
692 class FindLeakPaths {
693
694 @org.neo4j.procedure.Context
695 lateinit var transaction: Transaction
696
697 @UserFunction("shark.leakPaths")
leakPathsnull698 fun leakPaths(): List<Path> {
699 try {
700 val result = transaction.execute(
701 """
702 match (roots:GcRoots)
703 match (object:Object {leaked: true})
704 with shortestPath((roots)-[:ROOT|REF*]->(object)) as path
705 where reduce(
706 leakCount=0, n in nodes(path) | leakCount + case n.leaked when true then 1 else 0 end
707 ) = 1
708 return path
709 """.trimIndent()
710 )
711
712 return result.asSequence().map { row ->
713 val realPath = row["path"] as Path
714 DecoratedPath(realPath)
715 }.toList()
716 } catch (e: Throwable) {
717 TermUi.echo("failed to findLeakPaths: " + getStackTraceString(e))
718 throw e
719 }
720 }
721
getStackTraceStringnull722 private fun getStackTraceString(throwable: Throwable): String {
723 val stringWriter = StringWriter()
724 val printWriter = PrintWriter(stringWriter, false)
725 throwable.printStackTrace(printWriter)
726 printWriter.flush()
727 return stringWriter.toString()
728 }
729 }
730
<lambda>null731 class DecoratedPath(private val delegate: Path) : Path by delegate {
732
733 private val relationships by lazy {
734 // TODO Here we'll map a subset of relationships as one of not leaking, leak suspect, leaking.
735 // We can then add the "leaking reason" as an attribute of the relationship.
736 // Then we should remove these 2 from the full dump. We can find the leaking nodes early
737 // and set those attribute as part of node creation instead of a separate transaction.
738 // The mapping of relationships here can be down dy duplicating the logic in
739 // shark.HeapAnalyzer.computeLeakStatuses which goes through relationships and splits
740 // the path in 3 areas (not leaking, leak suspect, leaking).
741 delegate.relationships().toList()
742 }
743
744 override fun relationships(): Iterable<Relationship> {
745 return relationships
746 }
747
748 override fun reverseRelationships(): Iterable<Relationship> {
749 return relationships.asReversed()
750 }
751
752 override fun iterator(): MutableIterator<Entity> {
753 val nodeList = nodes().toList()
754 val relationshipsList = relationships
755 return (listOf(nodeList[0]) + relationshipsList.indices.flatMap { index ->
756 listOf(relationshipsList[index], nodeList[index])
757 }).toMutableList().iterator()
758 }
759 }
760