| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "bufio" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| "path/filepath" |
| "regexp" |
| "sort" |
| "strings" |
| ) |
| |
| type FileSystemSizes []FileSystemSize |
| |
| type FileSystemSize struct { |
| Name string `json:"name"` |
| Value json.Number `json:"value"` |
| Limit json.Number `json:"limit"` |
| } |
| |
| type SizeLimits struct { |
| // Specifies a size limit in bytes for ICU data files. |
| ICUDataLimit json.Number `json:"icu_data_limit"` |
| // Specifies a size limit in bytes for uncategorized packages. |
| CoreLimit json.Number `json:"core_limit"` |
| // Specifies the files that contribute to the ICU data limit. |
| ICUData []string `json:"icu_data"` |
| // Specifies the files that contributed to the distributed shared library |
| // size limits. |
| DistributedShlibs []string `json:"distributed_shlibs"` |
| // Specifies the distributed shared library size limit in bytes. |
| DistributedShlibsLimit json.Number `json:"distributed_shlibs_limit"` |
| // Specifies a series of size components/categories with a struct describing each. |
| Components []Component `json:"components"` |
| } |
| |
| // Display prints the contents of node and its children into a string. All |
| // strings are printed at the supplied indentation level. |
| type DisplayFn func(node *Node, level int) string |
| |
| type Node struct { |
| fullPath string |
| size int64 |
| copies int64 |
| parent *Node |
| children map[string]*Node |
| // display is a function used to print the contents of Node in a human-friendly way. |
| // If unset, a default display function is used. |
| display DisplayFn |
| } |
| |
| type Component struct { |
| Component string `json:"component"` |
| Limit json.Number `json:"limit"` |
| Src []string `json:"src"` |
| } |
| |
| type ComponentSize struct { |
| Size int64 `json:"size"` |
| Budget int64 `json:"budget"` |
| nodes []*Node |
| } |
| |
| type Blob struct { |
| dep []string |
| size int64 |
| name string |
| hash string |
| } |
| |
| type BlobFromSizes struct { |
| SourcePath string `json:"source_path"` |
| Merkle string `json:"merkle"` |
| Bytes int `json:"bytes"` |
| Size int `json:"size"` |
| } |
| |
| type BlobFromJSON struct { |
| Merkle string `json:"merkle"` |
| Path string `json:"path"` |
| SourcePath string `json:"source_path"` |
| } |
| |
| const ( |
| MetaFar = "meta.far" |
| PackageList = "gen/build/images/blob.manifest.list" |
| BlobsJSON = "blobs.json" |
| ConfigData = "config-data" |
| DataPrefix = "data/" |
| SizeCheckerJSON = "size_checker.json" |
| FileSystemSizesJSON = "filesystem_sizes.json" |
| ) |
| |
| func newDummyNode() *Node { |
| return newNode("") |
| } |
| |
| func newNode(p string) *Node { |
| return &Node{ |
| fullPath: p, |
| children: make(map[string]*Node), |
| } |
| } |
| |
| // newNodeWithDisplay creates a new Node, and supplies a custom function to be |
| // used to print its contents. |
| func newNodeWithDisplay(p string, display DisplayFn) *Node { |
| n := newNode(p) |
| n.display = display |
| return n |
| } |
| |
| // Breaks down the given path divided by "/" and updates the size of each node on the path with the |
| // size of the given blob. |
| func (root *Node) add(p string, blob *Blob) { |
| fullPath := strings.Split(p, "/") |
| curr := root |
| var nodePath string |
| // A blob may be used by many packages, so the size of each blob is the total space allocated to |
| // that blob in blobfs. |
| // We divide the size by the total number of packages that depend on it to get a rough estimate of |
| // the size of the individual blob. |
| size := blob.size / int64(len(blob.dep)) |
| curr.size += size |
| for _, name := range fullPath { |
| name = strings.TrimSuffix(name, ".meta") |
| nodePath = filepath.Join(nodePath, name) |
| if _, ok := curr.children[name]; !ok { |
| target := newNode(nodePath) |
| target.parent = curr |
| curr.children[name] = target |
| } |
| curr = curr.children[name] |
| curr.size += size |
| } |
| } |
| |
| // Finds the node whose fullPath matches the given path. The path is guaranteed to be unique as it |
| // corresponds to the filesystem of the build directory. Detaches the node from the tree. |
| func (root *Node) detachByPath(p string) *Node { |
| fullPath := strings.Split(p, "/") |
| curr := root |
| |
| for _, name := range fullPath { |
| next := curr.children[name] |
| if next == nil { |
| return nil |
| } |
| curr = next |
| } |
| curr.detach() |
| return curr |
| } |
| |
| // Detach this subtree from its parent, removing the size of this subtree from |
| // the aggregate size in the parent. |
| func (root *Node) detach() { |
| size := root.size |
| curr := root |
| for curr.parent != nil { |
| curr.parent.size -= size |
| curr = curr.parent |
| } |
| if root.parent != nil { |
| delete(root.parent.children, filepath.Base(root.fullPath)) |
| } |
| root.parent = nil |
| } |
| |
| // Returns the only child of a node. Useful for finding the root node. |
| func (node *Node) getOnlyChild() (*Node, error) { |
| if len(node.children) == 1 { |
| for _, child := range node.children { |
| return child, nil |
| } |
| } |
| |
| return nil, fmt.Errorf("this node does not contain a single child.") |
| } |
| |
| // displayAsDefault returns a human-readable representation of the supplied |
| // node and its children, using level as the indentation level for display |
| // pretty-printing. |
| func displayAsDefault(node *Node, level int) string { |
| var copies string |
| if node.copies > 1 { |
| copies = fmt.Sprintf("| %3d %-6s", node.copies, "copies") |
| } else if node.copies == 1 { |
| copies = fmt.Sprintf("| %3d %-6s", node.copies, "copy") |
| } |
| |
| var path = strings.TrimPrefix(node.fullPath, "obj/") |
| path = strings.TrimPrefix(path, "lib/") |
| if level > 1 { |
| path = filepath.Base(path) |
| } |
| var maxLen = 80 - 2*level |
| if maxLen < 0 { |
| maxLen = 0 |
| } |
| var pathLength = len(path) |
| if pathLength > maxLen { |
| var startPos = pathLength - maxLen + 3 |
| if startPos > pathLength { |
| startPos = pathLength |
| } |
| path = "..." + path[startPos:] |
| } |
| path = fmt.Sprintf("%s%s", strings.Repeat(" ", level), path) |
| ret := fmt.Sprintf("%-80s | %10s %10s\n", path, formatSize(node.size), copies) |
| |
| // Iterate over the childen in a sorted order. |
| keys := make([]string, 0, len(node.children)) |
| for k := range node.children { |
| keys = append(keys, k) |
| } |
| sort.Strings(keys) |
| |
| for _, k := range keys { |
| n := node.children[k] |
| ret += n.storageBreakdown(level + 1) |
| } |
| return ret |
| } |
| |
| // displayAsBlob returns a human-readable representation of the supplied Node and |
| // its children, formatted suitably for a blob ID, at the given indentation |
| // level. |
| func displayAsBlob(node *Node, level int) string { |
| nc := len(node.children) |
| ret := fmt.Sprintf("%vBlob ID %v (%v reuses):\n", strings.Repeat(" ", level), node.fullPath, nc) |
| |
| // Iterate over the childen in a sorted order. |
| keys := make([]string, 0, len(node.children)) |
| for k := range node.children { |
| keys = append(keys, k) |
| } |
| sort.Strings(keys) |
| |
| for _, k := range keys { |
| n := node.children[k] |
| ret += n.storageBreakdown(level + 1) |
| } |
| return ret |
| } |
| |
| // MetaRegex matches strings like: "/some/path/foo.meta/something_else" and |
| // grabs "foo" from them. |
| var metaRegex = regexp.MustCompile(`/([^/]+)\.meta/`) |
| |
| // displayAsMeta returns a human-readable representation of the supplied Node and |
| // its children, formatted suitably as a metadata item, at the given indentation |
| // level. |
| func displayAsMeta(node *Node, level int) string { |
| m := metaRegex.FindStringSubmatch(node.fullPath) |
| var n string |
| if len(m) == 0 || m[1] == "" { |
| n = node.fullPath |
| } else { |
| n = m[1] |
| } |
| ret := fmt.Sprintf("%s%s\n", strings.Repeat(" ", level), n) |
| // No children to iterate over. |
| return ret |
| } |
| |
| // storageBreakdown constructs a string including detailed storage |
| // consumption for the subtree of this node. |
| // |
| // `level` controls the indentation level to preserve hierarchy in the |
| // output. |
| func (node *Node) storageBreakdown(level int) string { |
| if node.display == nil { |
| // If the node has no custom display function. |
| return displayAsDefault(node, level) |
| } |
| return node.display(node, level) |
| } |
| |
| // Formats a given number into human friendly string representation of bytes, rounded to 2 decimal places. |
| func formatSize(sizeInBytes int64) string { |
| sizeInMiB := float64(sizeInBytes) / (1024 * 1024) |
| return fmt.Sprintf("%.2f MiB", sizeInMiB) |
| } |
| |
| // Extract all the packages from a given blob.manifest.list and blobs.json. |
| // It also returns a map containing all blobs, with the merkle root as the key. |
| func extractPackages(buildDir, packageListFileName, blobsJSON string) (blobMap map[string]*Blob, packages []string, err error) { |
| blobMap = make(map[string]*Blob) |
| |
| var merkleRootToSizeMap map[string]int64 |
| if merkleRootToSizeMap, err = openAndProcessBlobsJSON(filepath.Join(buildDir, blobsJSON)); err != nil { |
| return |
| } |
| |
| packageList, err := os.Open(filepath.Join(buildDir, packageListFileName)) |
| if err != nil { |
| return |
| } |
| defer packageList.Close() |
| |
| packageListScanner := bufio.NewScanner(packageList) |
| for packageListScanner.Scan() { |
| pkg, err := openAndParseBlobsManifest(blobMap, merkleRootToSizeMap, buildDir, packageListScanner.Text()) |
| if err != nil { |
| return blobMap, packages, err |
| } |
| |
| packages = append(packages, pkg...) |
| } |
| |
| return |
| } |
| |
| // Opens a blobs.manifest file to populate the blob map and extract all meta.far blobs. |
| // We expect each entry of blobs.manifest to have the following format: |
| // `$MERKLE_ROOT=$PATH_TO_BLOB` |
| func openAndParseBlobsManifest( |
| blobMap map[string]*Blob, |
| merkleRootToSizeMap map[string]int64, |
| buildDir, blobsManifestFileName string) ([]string, error) { |
| blobsManifestFile, err := os.Open(filepath.Join(buildDir, blobsManifestFileName)) |
| if err != nil { |
| return nil, err |
| } |
| defer blobsManifestFile.Close() |
| |
| return parseBlobsManifest(blobMap, merkleRootToSizeMap, blobsManifestFileName, blobsManifestFile), nil |
| } |
| |
| // Similar to openAndParseBlobsManifest, except it doesn't throw an I/O error. |
| func parseBlobsManifest( |
| merkleRootToBlobMap map[string]*Blob, |
| merkleRootToSizeMap map[string]int64, |
| blobsManifestFileName string, blobsManifestFile io.Reader) []string { |
| packages := []string{} |
| |
| blobsManifestScanner := bufio.NewScanner(blobsManifestFile) |
| for blobsManifestScanner.Scan() { |
| temp := strings.Split(blobsManifestScanner.Text(), "=") |
| merkleRoot := temp[0] |
| fileName := temp[1] |
| if blob, ok := merkleRootToBlobMap[merkleRoot]; !ok { |
| blob = &Blob{ |
| dep: []string{blobsManifestFileName}, |
| name: fileName, |
| size: merkleRootToSizeMap[merkleRoot], |
| hash: merkleRoot, |
| } |
| |
| merkleRootToBlobMap[merkleRoot] = blob |
| // This blob is a Fuchsia package. |
| if strings.HasSuffix(fileName, MetaFar) { |
| packages = append(packages, fileName) |
| } |
| } else { |
| blob.dep = append(blob.dep, blobsManifestFileName) |
| } |
| } |
| |
| return packages |
| } |
| |
| // Translates blobs.json into a map, with the key as the merkle root and the value as the size of |
| // that blob. |
| func openAndProcessBlobsJSON(blobsJSONFilePath string) (map[string]int64, error) { |
| blobsJSONFile, err := os.Open(blobsJSONFilePath) |
| if err != nil { |
| return nil, err |
| } |
| defer blobsJSONFile.Close() |
| |
| return processBlobsJSON(blobsJSONFile) |
| } |
| |
| func processBlobsJSON(blobsJSONFile io.Reader) (map[string]int64, error) { |
| blobs := []BlobFromSizes{} |
| if err := json.NewDecoder(blobsJSONFile).Decode(&blobs); err != nil { |
| return nil, err |
| } |
| |
| m := map[string]int64{} |
| for _, b := range blobs { |
| m[b.Merkle] = int64(b.Size) |
| } |
| return m, nil |
| } |
| |
| type processingState struct { |
| blobMap map[string]*Blob |
| icuDataMap map[string]*Node |
| distributedShlibs map[string]*Node |
| root *Node |
| } |
| |
| // Process the packages extracted from blob.manifest.list and process the blobs.json file to build a |
| // tree of packages. |
| func openAndParseBlobsJSON( |
| buildDir string, |
| packages []string, |
| state *processingState) error { |
| absBuildDir, err := filepath.Abs(buildDir) |
| if err != nil { |
| return fmt.Errorf("could not find abs path of directory: %v: %w", buildDir, err) |
| } |
| for _, metaFar := range packages { |
| // From the meta.far file, we can get the path to the blobs.json for that package. |
| dir := filepath.Dir(metaFar) |
| blobsJSON := filepath.Join(buildDir, dir, BlobsJSON) |
| // We then parse the blobs.json |
| blobs := []BlobFromJSON{} |
| data, err := ioutil.ReadFile(blobsJSON) |
| if err != nil { |
| return fmt.Errorf(readError(blobsJSON, err)) |
| } |
| if err := json.Unmarshal(data, &blobs); err != nil { |
| return fmt.Errorf(unmarshalError(blobsJSON, err)) |
| } |
| // Finally, we add the blob and the package to the tree. |
| parseBlobsJSON(state, blobs, dir, absBuildDir) |
| } |
| return nil |
| } |
| |
| // Similar to openAndParseBlobsJSON except it doesn't throw an I/O error. |
| func parseBlobsJSON( |
| state *processingState, |
| blobs []BlobFromJSON, |
| pkgPath string, |
| absBuildDir string) { |
| for _, blob := range blobs { |
| // If the blob is an ICU data file, we don't add it to the tree. |
| // We check the path instead of the source path because prebuilt packages have hashes as the |
| // source path for their blobs |
| baseBlobFilepath := filepath.Base(blob.Path) |
| |
| var ( |
| // Node must always be a pointer stored in state.icuDataMap, or nil. |
| node *Node |
| ok bool |
| ) |
| if node, ok = state.icuDataMap[baseBlobFilepath]; ok { |
| // The size of each blob is the total space occupied by the blob in blobfs (each blob may be |
| // referenced several times by different packages). Therefore, once we have already add the |
| // size, we should remove it from the map |
| if state.blobMap[blob.Merkle] != nil { |
| if node == nil { |
| state.icuDataMap[baseBlobFilepath] = newNode(baseBlobFilepath) |
| // Ensure that node remains a pointer into the map. |
| node = state.icuDataMap[baseBlobFilepath] |
| } |
| node.size += state.blobMap[blob.Merkle].size |
| node.copies += 1 |
| delete(state.blobMap, blob.Merkle) |
| } |
| // Save the full path of the ICU data file, so ICU data file |
| // proliferation can be debugged. |
| var blobNode *Node |
| blobNode = node.children[blob.Merkle] |
| if blobNode == nil { |
| blobNode = newNodeWithDisplay(blob.Merkle, displayAsBlob) |
| node.children[blob.Merkle] = blobNode |
| } |
| icuCopyNode := newNodeWithDisplay(blob.SourcePath, displayAsMeta) |
| blobNode.children[blob.SourcePath] = icuCopyNode |
| } else if node, ok = state.distributedShlibs[blob.Path]; ok { |
| if state.blobMap[blob.Merkle] != nil { |
| if node == nil { |
| state.distributedShlibs[blob.Path] = newNode(blob.Path) |
| node = state.distributedShlibs[blob.Path] |
| } |
| node.size += state.blobMap[blob.Merkle].size |
| node.copies += 1 |
| delete(state.blobMap, blob.Merkle) |
| } |
| } else { |
| var configPkgPath string |
| if filepath.Base(pkgPath) == ConfigData && strings.HasPrefix(blob.Path, DataPrefix) { |
| // If the package is config-data, we want to group the blobs by the name of the package |
| // the config data is for. |
| // By contract defined in config.gni, the path has the format 'data/$for_pkg/$outputs' |
| configPkgPath = strings.TrimPrefix(blob.Path, DataPrefix) |
| } |
| state.root.add(filepath.Join(pkgPath, configPkgPath), state.blobMap[blob.Merkle]) |
| } |
| } |
| } |
| |
| func parseBlobfsBudget(buildDir, fileSystemSizesJSONFilename string) int64 { |
| fileSystemSizesJSON := filepath.Join(buildDir, fileSystemSizesJSONFilename) |
| fileSystemSizesJSONData, err := ioutil.ReadFile(fileSystemSizesJSON) |
| if err != nil { |
| log.Fatal(readError(fileSystemSizesJSON, err)) |
| } |
| var fileSystemSizes = new(FileSystemSizes) |
| if err := json.Unmarshal(fileSystemSizesJSONData, &fileSystemSizes); err != nil { |
| log.Fatal(unmarshalError(fileSystemSizesJSON, err)) |
| } |
| for _, fileSystemSize := range *fileSystemSizes { |
| if fileSystemSize.Name == "blob/contents_size" { |
| budget, err := fileSystemSize.Limit.Int64() |
| if err != nil { |
| log.Fatalf("Failed to parse %s as an int64: %s\n", fileSystemSize.Limit, err) |
| } |
| return budget |
| } |
| } |
| return 0 |
| } |
| |
| // Processes the given sizeLimits and throws an error if any component in the sizeLimits is above its |
| // allocated space limit. |
| func parseSizeLimits(sizeLimits *SizeLimits, buildDir, packageList, blobsJSON string) map[string]*ComponentSize { |
| outputSizes := map[string]*ComponentSize{} |
| blobMap, packages, err := extractPackages(buildDir, packageList, blobsJSON) |
| if err != nil { |
| return outputSizes |
| } |
| |
| // We create a set of ICU data filenames. |
| icuDataMap := make(map[string]*Node) |
| for _, icu_data := range sizeLimits.ICUData { |
| icuDataMap[icu_data] = newNode(icu_data) |
| } |
| |
| // We also create a map of paths that should be considered distributed shlibs. |
| distributedShlibs := make(map[string]*Node) |
| for _, path := range sizeLimits.DistributedShlibs { |
| distributedShlibs[path] = newNode(path) |
| } |
| st := processingState{ |
| blobMap, |
| icuDataMap, |
| distributedShlibs, |
| // The dummy node will have the root node as its only child. |
| newDummyNode(), |
| } |
| // We process the meta.far files that were found in the blobs.manifest here. |
| if err := openAndParseBlobsJSON(buildDir, packages, &st); err != nil { |
| return outputSizes |
| } |
| |
| var distributedShlibsNodes []*Node |
| var totalDistributedShlibsSize int64 |
| for _, node := range st.distributedShlibs { |
| totalDistributedShlibsSize += node.size |
| distributedShlibsNodes = append(distributedShlibsNodes, node) |
| } |
| |
| var icuDataNodes []*Node |
| var totalIcuDataSize int64 |
| for _, node := range st.icuDataMap { |
| totalIcuDataSize += node.size |
| icuDataNodes = append(icuDataNodes, node) |
| } |
| |
| var total int64 |
| root, err := st.root.getOnlyChild() |
| if err != nil { |
| return outputSizes |
| } |
| |
| for _, component := range sizeLimits.Components { |
| var size int64 |
| var nodes []*Node |
| |
| for _, src := range component.Src { |
| if node := root.detachByPath(src); node != nil { |
| nodes = append(nodes, node) |
| size += node.size |
| } |
| } |
| total += size |
| budget, err := component.Limit.Int64() |
| if err != nil { |
| log.Fatalf("Failed to parse %s as an int64: %s\n", component.Limit, err) |
| } |
| |
| // There is only ever one copy of Update ZBIs. |
| if component.Component == "Update ZBIs" { |
| budget /= 2 |
| size /= 2 |
| } |
| outputSizes[component.Component] = &ComponentSize{ |
| Size: size, |
| Budget: budget, |
| nodes: nodes, |
| } |
| } |
| |
| ICUDataLimit, err := sizeLimits.ICUDataLimit.Int64() |
| if err != nil { |
| log.Fatalf("Failed to parse %s as an int64: %s\n", sizeLimits.ICUDataLimit, err) |
| } |
| const icuDataName = "ICU Data" |
| outputSizes[icuDataName] = &ComponentSize{ |
| Size: totalIcuDataSize, |
| Budget: ICUDataLimit, |
| nodes: icuDataNodes, |
| } |
| |
| CoreSizeLimit, err := sizeLimits.CoreLimit.Int64() |
| if err != nil { |
| log.Fatalf("Failed to parse %s as an int64: %s\n", sizeLimits.CoreLimit, err) |
| } |
| const coreName = "Core system+services" |
| coreNodes := make([]*Node, 0) |
| // `root` contains the leftover nodes that have not been detached by the path |
| // filters. |
| coreNodes = append(coreNodes, root) |
| outputSizes[coreName] = &ComponentSize{ |
| Size: root.size, |
| Budget: CoreSizeLimit, |
| nodes: coreNodes, |
| } |
| |
| if sizeLimits.DistributedShlibsLimit.String() != "" { |
| DistributedShlibsSizeLimit, err := sizeLimits.DistributedShlibsLimit.Int64() |
| if err != nil { |
| log.Fatalf("Failed to parse %s as an int64: %s\n", sizeLimits.DistributedShlibsLimit, err) |
| } |
| |
| const distributedShlibsName = "Distributed shared libraries" |
| outputSizes[distributedShlibsName] = &ComponentSize{ |
| Size: totalDistributedShlibsSize, |
| Budget: DistributedShlibsSizeLimit, |
| nodes: distributedShlibsNodes, |
| } |
| } |
| |
| return outputSizes |
| } |
| |
| func readError(file string, err error) string { |
| return verbError("read", file, err) |
| } |
| |
| func unmarshalError(file string, err error) string { |
| return verbError("unmarshal", file, err) |
| } |
| |
| func verbError(verb, file string, err error) string { |
| return fmt.Sprintf("Failed to %s %s: %s", verb, file, err) |
| } |
| |
| func writeOutputSizes(sizes map[string]*ComponentSize, outPath string) error { |
| f, err := os.Create(outPath) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| |
| encoder := json.NewEncoder(f) |
| encoder.SetIndent("", " ") |
| simpleSizes := make(map[string]interface{}) |
| budgetSuffix := ".budget" |
| // Use the size_limits gni file as the source of truth/reference for budget |
| // info until we're able to provide better contextual links. |
| ownerSuffix := ".owner" |
| for name, cs := range sizes { |
| simpleSizes[name] = cs.Size |
| simpleSizes[name+budgetSuffix] = cs.Budget |
| simpleSizes[name+ownerSuffix] = "http://goto.google.com/fuchsia-binary-size-limits" |
| } |
| if err := encoder.Encode(&simpleSizes); err != nil { |
| log.Fatal("failed to encode simpleSizes: ", err) |
| } |
| return nil |
| } |
| |
| func generateReport(outputSizes map[string]*ComponentSize, showBudgetOnly bool, ignorePerComponentBudget bool, blobFsBudget int64) (bool, string) { |
| var overBudget = false |
| var totalSize int64 = 0 |
| var totalBudget int64 = 0 |
| var totalRemaining int64 = 0 |
| var report strings.Builder |
| componentNames := make([]string, 0, len(outputSizes)) |
| for componentName := range outputSizes { |
| componentNames = append(componentNames, componentName) |
| } |
| sort.Strings(componentNames) |
| report.WriteString("\n") |
| report.WriteString(fmt.Sprintf("%-80s | %-10s | %-10s | %-10s\n", "Component", "Size", "Budget", "Remaining")) |
| report.WriteString(strings.Repeat("-", 119) + "\n") |
| for _, componentName := range componentNames { |
| var componentSize = outputSizes[componentName] |
| var remainingBudget = componentSize.Budget - componentSize.Size |
| var startColorCharacter string |
| var endColorCharacter string |
| |
| // If any component is overbudget, then size_checker will fail. |
| if componentSize.Size > componentSize.Budget && !ignorePerComponentBudget { |
| overBudget = true |
| } |
| |
| if showBudgetOnly { |
| if componentSize.Size > componentSize.Budget { |
| // Red |
| startColorCharacter = "\033[31m" |
| } else { |
| // Green |
| startColorCharacter = "\033[32m" |
| } |
| endColorCharacter = "\033[0m" |
| } |
| |
| totalSize += componentSize.Size |
| totalBudget += componentSize.Budget |
| totalRemaining += remainingBudget |
| report.WriteString( |
| fmt.Sprintf("%-80s | %10s | %10s | %s%10s%s\n", componentName, formatSize(componentSize.Size), formatSize(componentSize.Budget), startColorCharacter, formatSize(remainingBudget), endColorCharacter)) |
| if !showBudgetOnly { |
| for _, n := range componentSize.nodes { |
| report.WriteString(n.storageBreakdown(1)) |
| } |
| report.WriteString("\n") |
| } |
| |
| } |
| report.WriteString(strings.Repeat("-", 119) + "\n") |
| |
| report.WriteString(fmt.Sprintf("%-80s | %10s | %10s | %10s\n", "Total", formatSize(totalSize), formatSize(totalBudget), formatSize(totalRemaining))) |
| report.WriteString(fmt.Sprintf("%-80s | %10s | %10s | %10s\n", "Allocated System Data Budget", formatSize(totalBudget), formatSize(blobFsBudget), formatSize(blobFsBudget-totalBudget))) |
| |
| if totalSize > blobFsBudget { |
| report.WriteString( |
| fmt.Sprintf("ERROR: Total data size [%s] exceeds total system data budget [%s]\n", formatSize(totalSize), formatSize(blobFsBudget))) |
| overBudget = true |
| } |
| |
| if totalBudget > blobFsBudget && !ignorePerComponentBudget { |
| report.WriteString( |
| fmt.Sprintf("WARNING: Total per-component data budget [%s] exceeds total system data budget [%s]\n", formatSize(totalBudget), formatSize(blobFsBudget))) |
| overBudget = true |
| } |
| |
| return overBudget, report.String() |
| } |
| |
| func main() { |
| flag.Usage = func() { |
| fmt.Fprintln(os.Stderr, `Usage: size_checker [--budget-only] [--ignore-per-component-budget] --build-dir BUILD_DIR [--sizes-json-out SIZES_JSON] |
| |
| A executable that checks if any component from a build has exceeded its allocated space limit. |
| |
| See //tools/size_checker for more details.`) |
| flag.PrintDefaults() |
| } |
| var buildDir string |
| flag.StringVar(&buildDir, "build-dir", "", `(required) the output directory of a Fuchsia build.`) |
| var fileSizeOutPath string |
| flag.StringVar(&fileSizeOutPath, "sizes-json-out", "", "If set, will write a json object to this path with schema { <name (str)>: <file size (int)> }.") |
| var showBudgetOnly bool |
| flag.BoolVar(&showBudgetOnly, "budget-only", false, "If set, only budgets and total sizes of components will be shown.") |
| var ignorePerComponentBudget bool |
| flag.BoolVar(&ignorePerComponentBudget, "ignore-per-component-budget", false, |
| "If set, output will go to stderr only if the total size of components exceeds the total blobFs budget.") |
| |
| flag.Parse() |
| |
| if buildDir == "" { |
| flag.Usage() |
| os.Exit(2) |
| } |
| |
| sizeCheckerJSON := filepath.Join(buildDir, SizeCheckerJSON) |
| sizeCheckerJSONData, err := ioutil.ReadFile(sizeCheckerJSON) |
| if err != nil { |
| log.Fatal(readError(sizeCheckerJSON, err)) |
| } |
| var sizeLimits SizeLimits |
| if err := json.Unmarshal(sizeCheckerJSONData, &sizeLimits); err != nil { |
| log.Fatal(unmarshalError(sizeCheckerJSON, err)) |
| } |
| // If there are no components, then there are no work to do. We are done. |
| if len(sizeLimits.Components) == 0 { |
| os.Exit(0) |
| } |
| |
| outputSizes := parseSizeLimits(&sizeLimits, buildDir, PackageList, BlobsJSON) |
| if len(fileSizeOutPath) > 0 { |
| if err := writeOutputSizes(outputSizes, fileSizeOutPath); err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| blobFsBudget := parseBlobfsBudget(buildDir, FileSystemSizesJSON) |
| overBudget, report := generateReport(outputSizes, showBudgetOnly, ignorePerComponentBudget, blobFsBudget) |
| |
| if overBudget { |
| log.Fatal(report) |
| } else { |
| log.Println(report) |
| } |
| } |