blob: 5e40a63fa05bb75c75f9ef590a14d5a565594ac7 [file] [log] [blame]
<!--
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>