Merge changes I84bfadd3,I150677ff

* changes:
  Avoid showing too many entries in the pie chart once.
  Add support to analyse the disk usage by file extensions.
This commit is contained in:
Kelvin Zhang
2021-07-09 20:47:30 +00:00
committed by Gerrit Code Review
4 changed files with 294 additions and 13 deletions

View File

@@ -12,6 +12,16 @@
<button @click="updateChart('COWmerge')"> <button @click="updateChart('COWmerge')">
Analyse COW Merge Operations Analyse COW Merge Operations
</button> </button>
<BaseFile
label="Select The Target Android Build"
@file-select="selectBuild"
/>
<button
:disabled="!targetFile"
@click="updateChart('extensions')"
>
Analyse File Extensions
</button>
<div v-if="echartsData"> <div v-if="echartsData">
<PieChart :echartsData="echartsData" /> <PieChart :echartsData="echartsData" />
</div> </div>
@@ -20,6 +30,7 @@
<script> <script>
import PartialCheckbox from '@/components/PartialCheckbox.vue' import PartialCheckbox from '@/components/PartialCheckbox.vue'
import PieChart from '@/components/PieChart.vue' import PieChart from '@/components/PieChart.vue'
import BaseFile from '@/components/BaseFile.vue'
import { analysePartitions } from '../services/payload_composition.js' import { analysePartitions } from '../services/payload_composition.js'
import { chromeos_update_engine as update_metadata_pb } from '../services/update_metadata_pb.js' import { chromeos_update_engine as update_metadata_pb } from '../services/update_metadata_pb.js'
@@ -27,6 +38,7 @@ export default {
components: { components: {
PartialCheckbox, PartialCheckbox,
PieChart, PieChart,
BaseFile
}, },
props: { props: {
manifest: { manifest: {
@@ -39,6 +51,7 @@ export default {
partitionInclude: new Map(), partitionInclude: new Map(),
echartsData: null, echartsData: null,
listData: '', listData: '',
targetFile: null
} }
}, },
computed: { computed: {
@@ -49,15 +62,24 @@ export default {
}, },
}, },
methods: { methods: {
updateChart(metrics) { async updateChart(metrics) {
let partitionSelected = this.manifest.partitions.filter((partition) => let partitionSelected = this.manifest.partitions.filter((partition) =>
this.partitionInclude.get(partition.partitionName) this.partitionInclude.get(partition.partitionName)
) )
this.echartsData = analysePartitions( try {
metrics, this.echartsData = await analysePartitions(
partitionSelected, metrics,
this.manifest.blockSize) partitionSelected,
this.manifest.blockSize,
this.targetFile) }
catch (err) {
alert('Cannot be processed for the following issue: ', err)
}
}, },
selectBuild(files) {
//TODO(lishutong) check the version of target file is same to the OTA target
this.targetFile = files[0]
}
}, },
} }
</script> </script>

View File

@@ -5,11 +5,13 @@ export class EchartsData {
* @param {Map} statisticData * @param {Map} statisticData
* @param {String} title * @param {String} title
* @param {String} unit * @param {String} unit
* @param {Number} maximumEntries
*/ */
constructor(statisticData, title, unit) { constructor(statisticData, title, unit, maximumEntries = 15) {
this.statisticData = statisticData this.statisticData = statisticData
this.title = title this.title = title
this.unit = unit this.unit = unit
this.maximumEntries = maximumEntries
} }
/** /**
@@ -32,6 +34,9 @@ export class EchartsData {
* @return {Object} an ECharts option object. * @return {Object} an ECharts option object.
*/ */
getEchartsOption() { getEchartsOption() {
if (this.statisticData.size > this.maximumEntries) {
this.statisticData = trimMap(this.statisticData, this.maximumEntries)
}
let /** Object */ option = new Object() let /** Object */ option = new Object()
option.title = { option.title = {
text: this.title, text: this.title,
@@ -67,4 +72,39 @@ export class EchartsData {
] ]
return option return option
} }
}
/**
* When there are too many entries in the map, the pie chart can be very
* crowded. This function will return the entries that have high values.
* Specifically, the top <maximumEntries> will be stored and the others
* will be added into an entry called 'other'.
* @param {Map} map
* @param {Number} maximumEntries
* @return {Map}
*/
function trimMap(map, maximumEntries) {
if (map.size <= maximumEntries) return map
let /** Map */ new_map = new Map()
for (let i=0; i<maximumEntries; i++) {
let /** Number */ curr = 0
let /** String */ currKey = ''
for (let [key, value] of map) {
if (!new_map.get(key)) {
if (value > curr) {
curr = value
currKey = key
}
}
}
new_map.set(currKey, curr)
}
let /** Number */ restTotal = 0
for (let [key, value] of map) {
if (!new_map.get(key)) {
restTotal += value
}
}
new_map.set('other', restTotal)
return new_map
} }

View File

@@ -0,0 +1,119 @@
/**
* @fileoverview Class MapParser will take in a Android build and construct
* several file name maps (physical address: file name) according to it.
* The map of each partitions is added by calling MapParser.add(partitionName).
* You can query the file name being operated by calling
* MapParser.query(address, datalength).
*/
import * as zip from '@zip.js/zip.js/dist/zip-full.min.js'
export class MapParser {
/**
* This class will take in a .zip Android build and construct a file type map
* @param {File} targetFile
*/
constructor(targetFile) {
this.build = new zip.ZipReader(new zip.BlobReader(targetFile))
this.mapFiles = new Map()
this.maps = new Map()
}
/**
* Find the .map entries in the .zip build file. Store them as a map with
* pairs of (partition name: zip.js entry).
*/
async init() {
let /** Array<Entry> */ entries = await this.build.getEntries()
const /** RegExp*/ regexPath = /IMAGES\/[a-z_]*\.map/g;
const /** RegExp*/ regexName = /[\w_]+(?=\.map)/g
entries.forEach((entry) => {
if (entry.filename.match(regexPath)) {
this.mapFiles.set(entry.filename.match(regexName)[0], entry)
}
});
}
/**
* According to the .map in the build, build a map for later query.
* @param {String} partitionName
* @param {Number} totalLength
*/
async add(partitionName, totalLength) {
let /** Array<String> */ map = []
const /** RegExp */ regexNumber = /(?<![0-9\-])\d+(?![0-9\-])/g
const /** Reg */ regexRange = /\d+\-\d+/g
for (let i = 0; i < totalLength; i++) map[i] = 'unknown'
if (this.mapFiles.get(partitionName)) {
let /** String */mapText =
await this.mapFiles.get(partitionName).getData(
new zip.TextWriter()
)
let /** Array<String> */fileEntries = mapText.split('\n')
// Each line of the .map file in Android build starts with the filename
// Followed by the block address, either a number or a range, for example:
// //system/apex/com.android.adbd.apex 54-66 66 66-2663
for (let entry of fileEntries) {
let /** Array<String> */ elements = entry.split(' ')
for (let j = 1; j < elements.length; j++) {
let /** Number */ left = 0
let /** Number */ right = 0
if (elements[j].match(regexRange)) {
left = parseInt(elements[j].match(/\d+/g)[0])
right = parseInt(elements[j].match(/\d+/g)[1])
} else {
left = parseInt(elements[j].match(regexNumber))
right = parseInt(elements[j].match(regexNumber))
}
InsertMap(map, elements[0], left, right)
}
}
this.maps.set(partitionName, map)
}
else {
this.maps.set(partitionName, map)
}
}
/**
* Return the filename of given address.
* @param {String} partitionName
* @param {Array<PartitionUpdate>} extents
* @return {Array<String>}
*/
query(partitionName, extents) {
let /** Array<String> */ names = []
let /** Array<String> */ map = this.maps.get(partitionName)
for (let ext of extents) {
names.push(queryMap(map,
ext.startBlock,
ext.startBlock + ext.numBlocks))
}
return names
}
}
/**
* Fill in the hashtable from <left> to <right> using <name>.
* @param {Array<String>} map
* @param {String} name
* @param {Number} left
* @param {Number} right
*/
function InsertMap(map, name, left, right) {
for (let i = left; i <= right; i++) {
map[i] = name
}
}
/**
* Query the hashtable <map> using index <address>.
* @param {Array<String>} map
* @param {Number} left
* @param {Number} right
*/
function queryMap(map, left, right) {
// Assuming the consecutive blocks belong to the same file
// Only the start block is queried here.
return map[left]
}

View File

@@ -5,14 +5,15 @@
*/ */
import { OpType, MergeOpType } from '@/services/payload.js' import { OpType, MergeOpType } from '@/services/payload.js'
import { EchartsData } from '../services/echarts_data.js' import { EchartsData } from '@/services/echarts_data.js'
import { MapParser } from '@/services/map_parser.js'
/** /**
* Add a <value> to a element associated to <key>. If the element dose not * Add a <value> to a element associated to <key>. If the element dose not
* exists than its value will be initialized to zero. * exists than its value will be initialized to zero.
* @param {Map} map * @param {Map} map
* @param {String} key * @param {String} key
* @param {Nynber} value * @param {Number} value
*/ */
function addNumberToMap(map, key, value) { function addNumberToMap(map, key, value) {
if (!map.get(key)) { if (!map.get(key)) {
@@ -55,12 +56,13 @@ export function mergeOperationStatistics(partitions, blockSize) {
operationType, operationType,
operation.dstExtent.numBlocks) operation.dstExtent.numBlocks)
} }
totalBlocks += partition.newPartitionInfo.size / blockSize // The total blocks number should be rounded up
totalBlocks += Math.ceil(partition.newPartitionInfo.size / blockSize)
} }
// The COW merge operation is default to be COW_replace and not shown in // The COW merge operation is default to be COW_replace and not shown in
// the manifest info. We have to mannually add that part of operations, // the manifest info. We have to mannually add that part of operations,
// by subtracting the total blocks with other blocks. // by subtracting the total blocks with other blocks.
mergeOperations.forEach((value, key)=> totalBlocks -= value ) mergeOperations.forEach((value, key) => totalBlocks -= value)
mergeOperations.set('COW_REPLACE', totalBlocks) mergeOperations.set('COW_REPLACE', totalBlocks)
return mergeOperations return mergeOperations
} }
@@ -87,13 +89,55 @@ export function operatedPayloadStatistics(partitions) {
return operatedBlocks return operatedBlocks
} }
/**
* Return a statistics over the disk usage of each file types in a OTA package.
* A target file has to be provided and address-filename maps will be built.
* Only partitions that are being passed in will be included.
* @param {Array<PartitionUpdate>} partitions
* @param {Number} blockSize
* @param {File} targetFile
* @return {Map}
*/
export async function operatedExtensionStatistics(partitions, blockSize, targetFile) {
let /** Map */ operatedExtensions = new Map()
if (!targetFile) {
return operatedExtensions
}
let buildMap = new MapParser(targetFile)
await buildMap.init()
for (let partition of partitions) {
await buildMap.add(
partition.partitionName,
Math.ceil(partition.newPartitionInfo.size / blockSize))
for (let operation of partition.operations) {
if (!operation.hasOwnProperty('dataLength')) continue
let operatedFileNames = buildMap.query(
partition.partitionName,
operation.dstExtents)
let extentDataLength = distributeExtensions(
operatedFileNames,
operation.dstExtents,
operation.dataLength
)
extentDataLength.forEach((value, key) => {
addNumberToMap(
operatedExtensions,
key,
value
)
})
}
}
return operatedExtensions
}
/** /**
* Analyse the given partitions using the given metrics. * Analyse the given partitions using the given metrics.
* @param {String} metrics * @param {String} metrics
* @param {Array<PartitionUpdate>} partitions * @param {Array<PartitionUpdate>} partitions
* @return {EchartsData} * @return {EchartsData}
*/ */
export function analysePartitions(metrics, partitions, blockSize=4096) { export async function analysePartitions(metrics, partitions, blockSize = 4096, targetFile = null) {
let /** Map */statisticsData let /** Map */statisticsData
let /** Echartsdata */ echartsData let /** Echartsdata */ echartsData
switch (metrics) { switch (metrics) {
@@ -120,8 +164,25 @@ export function analysePartitions(metrics, partitions, blockSize=4096) {
'COW merge operations', 'COW merge operations',
'blocks' 'blocks'
) )
break
case 'extensions':
try {
statisticsData = await operatedExtensionStatistics(partitions, blockSize, targetFile)
}
catch (err) {
throw err
}
echartsData = new EchartsData(
statisticsData,
'Size of operated filename extensions',
'bytes'
)
}
if (echartsData) {
return echartsData
} else {
throw 'Please double check if this is a proper AB OTA package.'
} }
return echartsData
} }
/** /**
@@ -137,11 +198,50 @@ export function numBlocks(exts) {
/** /**
* Return a string that indicates the blocks being operated * Return a string that indicates the blocks being operated
* in the manner of (start_block, block_length) * in the manner of (start_block, block_length)
* @param {Array<InstallOperations} exts * @param {Array<InstallOperations>} exts
* @return {string} * @return {string}
*/ */
export function displayBlocks(exts) { export function displayBlocks(exts) {
const accumulator = (total, ext) => const accumulator = (total, ext) =>
total + '(' + ext.startBlock + ',' + ext.numBlocks + ')' total + '(' + ext.startBlock + ',' + ext.numBlocks + ')'
return exts.reduce(accumulator, '') return exts.reduce(accumulator, '')
}
/**
* Return a map with pairs of (file extension, data length used by this
* extension). The total data length will be distributed by the blocks ratio
* of each extent.
* @param {Array<String>} filenames
* @param {Array<InstallOperations>} exts
* @param {Number} length
* @return {Map}
*/
export function distributeExtensions(filenames, exts, length) {
let totalBlocks = numBlocks(exts)
let distributedLengths = new Map()
for (let i = 0; i < filenames.length; i++) {
addNumberToMap(
distributedLengths,
name2Extension(filenames[i]),
Math.round(length * exts[i].numBlocks / totalBlocks)
)
}
return distributedLengths
}
/**
* convert a filename into extension, for example:
* '//system/apex/com.android.adbd.apex' => 'apex'
* @param {String} filename
* @return {String}
*/
export function name2Extension(filename) {
let elements = filename.split('.')
if (elements.length>1) {
return elements[elements.length - 1]
} else if (elements[0]==='unknown') {
return 'unknown'
} else {
return 'no-extension'
}
} }