• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<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