Merge changes I84bfadd3,I150677ff am: ffdba5d2c5 am: dffd57e63c
Original change: https://android-review.googlesource.com/c/platform/development/+/1761689 Change-Id: Ie15bd2ef72d22428c79c22d0eb1838a81feeaada
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
119
tools/ota_analysis/src/services/map_parser.js
Normal file
119
tools/ota_analysis/src/services/map_parser.js
Normal 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]
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user