| <!-- |
| Copyright (c) 2020 Contributors to the Eclipse Foundation |
| |
| See the NOTICE file(s) distributed with this work for additional |
| information regarding copyright ownership. |
| |
| This program and the accompanying materials are made available under the |
| terms of the Eclipse Public License 2.0 which is available at |
| http://www.eclipse.org/legal/epl-2.0 |
| |
| SPDX-License-Identifier: EPL-2.0 |
| --> |
| <template xmlns:v-contextmenu="http://www.w3.org/1999/xhtml"> |
| <div class="resultDiv"> |
| <v-contextmenu ref="contextmenu"> |
| <v-contextmenu-submenu :title="$t('jifa.heap.ref.object.label')"> |
| <v-contextmenu-item |
| @click="$emit('outgoingRefsOfObj', contextMenuTargetObjectId, contextMenuTargetObjectLabel)"> |
| {{$t('jifa.heap.ref.object.outgoing')}} |
| </v-contextmenu-item> |
| <v-contextmenu-item |
| @click="$emit('incomingRefsOfObj', contextMenuTargetObjectId, contextMenuTargetObjectLabel)"> |
| {{$t('jifa.heap.ref.object.incoming')}} |
| </v-contextmenu-item> |
| </v-contextmenu-submenu> |
| <v-contextmenu-submenu :title="$t('jifa.heap.ref.type.label')"> |
| <v-contextmenu-item |
| @click="$emit('outgoingRefsOfClass', contextMenuTargetObjectId, contextMenuTargetObjectLabel)"> |
| {{$t('jifa.heap.ref.type.outgoing')}} |
| </v-contextmenu-item> |
| <v-contextmenu-item |
| @click="$emit('incomingRefsOfClass', contextMenuTargetObjectId, contextMenuTargetObjectLabel)"> |
| {{$t('jifa.heap.ref.type.incoming')}} |
| </v-contextmenu-item> |
| </v-contextmenu-submenu> |
| <v-contextmenu-item divider></v-contextmenu-item> |
| <v-contextmenu-item |
| @click="$emit('pathToGCRootsOfObj', contextMenuTargetObjectId, contextMenuTargetObjectLabel)"> |
| {{$t('jifa.heap.pathToGCRoots')}} |
| </v-contextmenu-item> |
| </v-contextmenu> |
| |
| <el-tabs v-model="editableTabsValue" type="card" closable @tab-remove="removeTab" style="height: 100%"> |
| <el-tab-pane |
| v-for="(item) in editableTabs" |
| :key="item.name" |
| :label="item.title" |
| :name="item.name"> |
| <el-table v-if="item.isObjRefsTab" |
| ref="resultContainer" |
| v-loading="item.loading" |
| :data="item.topItems" |
| :highlight-current-row="false" |
| stripe |
| :header-cell-style="headerCellStyle" |
| :cell-style='cellStyle' |
| :load="item.load" |
| row-key="rowKey" |
| lazy |
| :indent=8 |
| height="100%"> |
| <el-table-column label="Class Name" width="1000px" show-overflow-tooltip> |
| <template slot-scope="scope"> |
| <span v-if="scope.row.isResult" @click="$emit('setSelectedObjectId', scope.row.objectId)" |
| style="cursor: pointer" |
| @contextmenu="contextMenuTargetObjectId = scope.row.objectId; contextMenuTargetObjectLabel = scope.row.label" |
| v-contextmenu:contextmenu> |
| <img :src="scope.row.icon" style="margin-right: 5px"/> |
| <strong>{{ scope.row.prefix }}</strong> |
| {{ scope.row.label }} |
| <span style="font-weight: bold; color: #909399"> |
| {{ scope.row.suffix }} |
| </span> |
| </span> |
| |
| <span v-if="scope.row.isBoundsSummary"> |
| <img :src="ICONS.misc.sumIcon" v-if="scope.row.currentSize >= scope.row.totalSize"/> |
| <img :src="ICONS.misc.sumPlusIcon" |
| @dblclick="fetchObjBounds(scope.row.parentRowKey, scope.row.objectId, scope.row.nextPage, scope.row.resolve)" |
| style="cursor: pointer" |
| v-else/> |
| {{ scope.row.currentSize }} <strong> / </strong> {{ scope.row.totalSize }} |
| </span> |
| </template> |
| </el-table-column> |
| <el-table-column label="Shallow Heap" prop="shallowHeap"> |
| </el-table-column> |
| <el-table-column label="Retained Heap" prop="retainedHeap"> |
| </el-table-column> |
| </el-table> |
| |
| |
| <el-table v-if="item.isClassRefsTab" |
| ref="resultContainer" |
| v-loading="item.loading" |
| :data="item.topItems" |
| :highlight-current-row="false" |
| stripe |
| :header-cell-style="headerCellStyle" |
| :cell-style='cellStyle' |
| :load="item.load" |
| row-key="rowKey" |
| lazy |
| :indent=8 |
| height="100%"> |
| <el-table-column label="Class Name" width="1000px" show-overflow-tooltip> |
| <template slot-scope="scope"> |
| <span v-if="scope.row.isResult" @click="$emit('setSelectedObjectId', scope.row.objectId)" |
| style="cursor: pointer" |
| @contextmenu="contextMenuTargetObjectId = scope.row.objectId; contextMenuTargetObjectLabel = scope.row.label" |
| v-contextmenu:contextmenu> |
| <img :src="scope.row.icon" style="margin-right: 5px"/> |
| {{ scope.row.label }} |
| </span> |
| |
| <span v-if="scope.row.isBoundsSummary"> |
| <img :src="ICONS.misc.sumIcon" v-if="scope.row.currentSize >= scope.row.totalSize"/> |
| <img :src="ICONS.misc.sumPlusIcon" |
| @dblclick="fetchClassBounds(scope.row.parentRowKey, scope.row.objectIds, scope.row.nextPage, scope.row.resolve)" |
| style="cursor: pointer" |
| v-else/> |
| {{ scope.row.currentSize }} <strong> / </strong> {{ scope.row.totalSize }} |
| </span> |
| </template> |
| </el-table-column> |
| <el-table-column label="Objects" prop="objects"> |
| </el-table-column> |
| <el-table-column label="Shallow Heap" prop="shallowHeap"> |
| </el-table-column> |
| </el-table> |
| |
| <div v-if="item.isPathToGCRootsTab" style="height: 100%; overflow: scroll"> |
| <el-tree ref="resultContainer" |
| v-loading="item.loading" |
| :data="item.treeData" |
| node-key="objectId" |
| :expand-on-click-node="false" |
| default-expand-all> |
| <span class="custom-tree-node" style="font-size: 12px" slot-scope="{ node, data }" |
| @click="$emit('setSelectedObjectId', data.objectId)" |
| @contextmenu="contextMenuTargetObjectId = data.objectId; contextMenuTargetObjectLabel = data.label" |
| v-contextmenu:contextmenu> |
| <span> |
| <img :src="data.origin ? getIcon(data.gCRoot, data.objectType) : getInboundIcon(data.gCRoot, data.objectType)" |
| style="margin-right: 5px"/> |
| <strong>{{ data.prefix }}</strong> |
| {{ data.label }} |
| <span style="font-weight: bold; color: #909399"> |
| {{ data.suffix }} |
| </span> |
| </span> |
| |
| <span> |
| {{ data.shallowSize }} / {{ data.retainedSize }} |
| </span> |
| </span> |
| </el-tree> |
| <el-divider><i v-if="item.hasMore" :class="item.loading ? 'el-icon-loading':'el-icon-circle-plus-outline'" |
| style="font-size: 25px; cursor: pointer" |
| @dblclick="loadMorePathToGCRootsOfObj(item)"></i></el-divider> |
| </div> |
| </el-tab-pane> |
| </el-tabs> |
| </div> |
| </template> |
| <script> |
| import axios from 'axios' |
| import { |
| getClassRefInboundIcon, |
| getClassRefOutboundIcon, |
| getIcon, |
| getInboundIcon, |
| getOutboundIcon, |
| ICONS |
| } from "./IconHealper"; |
| import {heapDumpService} from "../../util"; |
| |
| let rowKey = 1 |
| |
| export default { |
| props: ['file'], |
| data() { |
| return { |
| cellStyle: {padding: '4px', fontSize: '12px'}, |
| headerCellStyle: {padding: 0, 'font-size': '12px', 'font-weight': 'normal'}, |
| tabIndex: 0, |
| editableTabsValue: null, |
| editableTabs: [], |
| ICONS, |
| pageSize: 25, |
| |
| contextMenuTargetObjectId: null, |
| contextMenuTargetObjectLabel: null, |
| } |
| }, |
| methods: { |
| getIcon, |
| getInboundIcon, |
| getOutboundIcon, |
| currentIndex() { |
| for (let i = 0; i < this.editableTabs.length; i++) { |
| if (this.editableTabs[i].name === this.editableTabsValue) { |
| return i; |
| } |
| } |
| }, |
| buildTitle(prefix, label) { |
| return '「' + prefix + '」' + label |
| }, |
| |
| loadObjBounds(tree, treeNode, resolve) { |
| this.fetchObjBounds(tree.rowKey, tree.objectId, 1, resolve) |
| }, |
| |
| fetchObjBounds(parentRowKey, objectId, page, resolve) { |
| let index = this.currentIndex() |
| let tab = this.editableTabs[index] |
| let outbound = tab.outbound |
| let table = this.$refs.resultContainer[index] |
| tab.loading = true |
| axios.get(heapDumpService(this.file, outbound ? 'outbounds' : 'inbounds'), { |
| params: { |
| objectId: objectId, |
| page: page, |
| pageSize: this.pageSize, |
| } |
| }).then(resp => { |
| let loadedLen = 0; |
| let loaded = table.store.states.lazyTreeNodeMap[parentRowKey] |
| let callResolve = false |
| if (loaded) { |
| loadedLen = loaded.length |
| if (loadedLen > 0) { |
| loaded.splice(--loadedLen, 1) |
| } |
| } else { |
| loaded = [] |
| callResolve = true; |
| } |
| |
| let res = resp.data.data |
| res.forEach(d => { |
| loaded.push({ |
| rowKey: rowKey++, |
| icon: outbound ? getOutboundIcon(d.gCRoot, d.objectType) : getInboundIcon(d.gCRoot, d.objectType), |
| prefix: d.prefix, |
| label: d.label, |
| suffix: d.suffix, |
| shallowHeap: d.shallowSize, |
| retainedHeap: d.retainedSize, |
| hasChildren: true, |
| objectId: d.objectId, |
| isResult: true |
| }) |
| }) |
| |
| loaded.push({ |
| rowKey: rowKey++, |
| objectId: objectId, |
| parentRowKey: parentRowKey, |
| isBoundsSummary: true, |
| nextPage: page + 1, |
| currentSize: loadedLen + res.length, |
| totalSize: resp.data.totalSize, |
| resolve: resolve, |
| }) |
| |
| if (callResolve) { |
| resolve(loaded) |
| } |
| tab.loading = false |
| }) |
| }, |
| |
| outgoingRefsOfObj(objectId, label) { |
| this.boundsOfObj(objectId, label, true) |
| }, |
| |
| incomingRefsOfObj(objectId, label) { |
| this.boundsOfObj(objectId, label, false) |
| }, |
| |
| boundsOfObj(objectId, label, outbound) { |
| let newTabName = ++this.tabIndex + ''; |
| let newTab = this.addObjRefsTab(this.buildTitle(this.$t(outbound ? |
| 'jifa.heap.ref.object.outgoing' : 'jifa.heap.ref.object.incoming'), label), |
| newTabName, this.loadObjBounds, outbound) |
| newTab.loading = true |
| axios.get(heapDumpService(this.file, 'object'), { |
| params: { |
| objectId: objectId |
| } |
| }).then((resp) => { |
| let topItems = []; |
| let d = resp.data |
| topItems.push( |
| { |
| rowKey: rowKey++, |
| icon: outbound ? getOutboundIcon(d.gCRoot, d.objectType) : getIcon(d.gCRoot, d.objectType), |
| label: d.label, |
| suffix: d.suffix, |
| shallowHeap: d.shallowSize, |
| retainedHeap: d.retainedSize, |
| hasChildren: true, |
| objectId: d.objectId, |
| isResult: true |
| } |
| ) |
| newTab.topItems = topItems |
| newTab.loading = false |
| } |
| ) |
| }, |
| |
| addObjRefsTab(title, name, load, outbound) { |
| this.tabIndex++; |
| let newTab = { |
| title: title, |
| name: name, |
| loading: false, |
| topItems: [], |
| load: load, |
| outbound: outbound, |
| isObjRefsTab: true |
| } |
| this.editableTabs.push(newTab) |
| this.editableTabsValue = name; |
| return newTab |
| }, |
| |
| loadClassBounds(tree, treeNode, resolve) { |
| this.fetchClassBounds(tree.rowKey, tree.objectIds, 1, resolve) |
| }, |
| |
| fetchClassBounds(parentRowKey, objectIds, page, resolve) { |
| let index = this.currentIndex() |
| let tab = this.editableTabs[index] |
| let outbound = tab.outbound |
| let table = this.$refs.resultContainer[index] |
| tab.loading = true |
| axios.get(heapDumpService(this.file, outbound ? 'classReference/outbounds/children' : 'classReference/inbounds/children'), { |
| params: { |
| objectIds: JSON.stringify(objectIds), |
| page: page, |
| pageSize: this.pageSize, |
| } |
| }).then(resp => { |
| let loadedLen = 0; |
| let loaded = table.store.states.lazyTreeNodeMap[parentRowKey] |
| let callResolve = false |
| if (loaded) { |
| loadedLen = loaded.length |
| if (loadedLen > 0) { |
| loaded.splice(--loadedLen, 1) |
| } |
| } else { |
| loaded = [] |
| callResolve = true; |
| } |
| |
| let res = resp.data.data |
| res.forEach(record => { |
| loaded.push({ |
| rowKey: rowKey++, |
| label: record.label, |
| hasChildren: true, |
| objectId: record.objectId, |
| objectIds: record.objectIds, |
| objects: record.objects, |
| shallowHeap: record.shallowSize, |
| icon: outbound ? getClassRefOutboundIcon(record.type) : getClassRefInboundIcon(record.type), |
| isResult: true |
| }) |
| }) |
| |
| loaded.push({ |
| rowKey: rowKey++, |
| objectIds: objectIds, |
| parentRowKey: parentRowKey, |
| isBoundsSummary: true, |
| nextPage: page + 1, |
| currentSize: loadedLen + res.length, |
| totalSize: resp.data.totalSize, |
| resolve: resolve, |
| }) |
| |
| if (callResolve) { |
| resolve(loaded) |
| } |
| tab.loading = false |
| }) |
| }, |
| |
| outgoingRefsOfClass(objectId, label) { |
| this.boundsOfClass(objectId, label, true) |
| }, |
| |
| incomingRefsOfClass(objectId, label) { |
| this.boundsOfClass(objectId, label, false) |
| }, |
| |
| boundsOfClass(id, label, outbound) { |
| let newTabName = ++this.tabIndex + ''; |
| let newTab = this.addClassRefsTab(this.buildTitle(this.$t(outbound ? |
| 'jifa.heap.ref.type.outgoing' : 'jifa.heap.ref.type.incoming'), label), |
| newTabName, this.loadClassBounds, outbound) |
| newTab.loading = true |
| axios.get(heapDumpService(this.file, outbound ? 'classReference/outbounds/class' : 'classReference/inbounds/class'), { |
| params: { |
| objectId: id, |
| } |
| }).then((resp) => { |
| let topItems = []; |
| let record = resp.data |
| topItems.push( |
| { |
| rowKey: rowKey++, |
| label: record.label, |
| hasChildren: true, |
| objectId: record.objectId, |
| objectIds: record.objectIds, |
| objects: record.objects, |
| shallowHeap: record.shallowSize, |
| icon: outbound ? getClassRefOutboundIcon(record.type) : ICONS.objects.class, |
| isResult: true |
| } |
| ) |
| newTab.topItems = topItems |
| newTab.loading = false |
| } |
| ) |
| }, |
| |
| addClassRefsTab(title, name, load, outbound) { |
| this.tabIndex++; |
| let newTab = { |
| title: title, |
| name: name, |
| loading: false, |
| topItems: [], |
| load: load, |
| outbound: outbound, |
| isClassRefsTab: true |
| } |
| this.editableTabs.push(newTab) |
| this.editableTabsValue = name; |
| return newTab |
| }, |
| |
| pathToGCRootsOfObj(objectId, label) { |
| let newTabName = ++this.tabIndex + ''; |
| this.loadMorePathToGCRootsOfObj(this.addPathToGCRootsTab(this.buildTitle(this.$t('jifa.heap.pathToGCRoots'), label), |
| newTabName, objectId)) |
| }, |
| |
| loadMorePathToGCRootsOfObj(tab) { |
| tab.loading = true |
| axios.get(heapDumpService(this.file, 'pathToGCRoots'), { |
| params: { |
| skip: tab.count, |
| origin: tab.origin, |
| count: 10 |
| } |
| }).then((resp) => { |
| tab.count += resp.data.count |
| tab.hasMore = resp.data.hasMore |
| if (tab.treeData.length === 0) { |
| // first |
| tab.treeData.push(resp.data.tree) |
| } else { |
| this.mergePath(resp.data.tree.children, tab.treeData[0]) |
| } |
| tab.loading = false |
| }) |
| }, |
| |
| mergePath(children, parent) { |
| for (let i = 0; i < children.length; i++) { |
| let found = false |
| let child = children[i] |
| for (let j = 0; j < parent.children.length; j++) { |
| let oldChild = parent.children[j] |
| if (oldChild.objectId === child.objectId) { |
| found = true |
| this.mergePath(child.children, oldChild) |
| break; |
| } |
| } |
| |
| if (!found) { |
| parent.children.push(child) |
| } |
| } |
| }, |
| |
| addPathToGCRootsTab(title, name, objectId) { |
| this.tabIndex++; |
| let newTab = { |
| title: title, |
| name: name, |
| loading: false, |
| isPathToGCRootsTab: true, |
| origin: objectId, |
| treeData: [], |
| hasMore: false, |
| count: 0, |
| } |
| this.editableTabs.push(newTab) |
| this.editableTabsValue = name; |
| return newTab |
| }, |
| |
| removeTab(targetName) { |
| let tabs = this.editableTabs; |
| let activeName = this.editableTabsValue; |
| if (activeName === targetName) { |
| tabs.forEach((tab, index) => { |
| if (tab.name === targetName) { |
| let nextTab = tabs[index + 1] || tabs[index - 1]; |
| if (nextTab) { |
| activeName = nextTab.name; |
| } |
| } |
| }); |
| } |
| |
| this.editableTabsValue = activeName; |
| this.editableTabs = tabs.filter(tab => tab.name !== targetName); |
| } |
| }, |
| watch: { |
| editableTabs() { |
| if (this.editableTabs.length === 0) { |
| this.$emit('disableShowDynamicResultSlot') |
| } |
| } |
| } |
| } |
| </script> |
| |
| <style scoped> |
| .custom-tree-node { |
| flex: 1; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| font-size: 14px; |
| padding-right: 8px; |
| } |
| </style> |
| |
| <style> |
| .resultDiv { |
| height: 100%; |
| position: relative |
| } |
| </style> |