Home | History | Annotate | Download | only in src
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 import org.w3c.dom.Document;
     18 import org.w3c.dom.Element;
     19 import org.w3c.dom.NamedNodeMap;
     20 import org.w3c.dom.Node;
     21 import org.w3c.dom.NodeList;
     22 import org.xml.sax.InputSource;
     23 import org.xml.sax.SAXException;
     24 
     25 import java.io.BufferedReader;
     26 import java.io.BufferedWriter;
     27 import java.io.File;
     28 import java.io.FileNotFoundException;
     29 import java.io.FileReader;
     30 import java.io.FileWriter;
     31 import java.io.IOException;
     32 import java.io.Reader;
     33 import java.io.StringReader;
     34 import java.util.ArrayList;
     35 import java.util.Collections;
     36 import java.util.HashMap;
     37 import java.util.HashSet;
     38 import java.util.List;
     39 import java.util.Map;
     40 import java.util.Map.Entry;
     41 import java.util.Set;
     42 
     43 import javax.xml.parsers.DocumentBuilder;
     44 import javax.xml.parsers.DocumentBuilderFactory;
     45 import javax.xml.parsers.ParserConfigurationException;
     46 
     47 /**
     48  * Gathers statistics about attribute usage in layout files. This is how the "topAttrs"
     49  * attributes listed in ADT's extra-view-metadata.xml (which drives the common attributes
     50  * listed in the top of the context menu) is determined by running this script on a body
     51  * of sample layout code.
     52  * <p>
     53  * This program takes one or more directory paths, and then it searches all of them recursively
     54  * for layout files that are not in folders containing the string "test", and computes and
     55  * prints frequency statistics.
     56  */
     57 public class Analyzer {
     58     /** Number of attributes to print for each view */
     59     public static final int ATTRIBUTE_COUNT = 6;
     60     /** Separate out any attributes that constitute less than N percent of the total */
     61     public static final int THRESHOLD = 10; // percent
     62 
     63     private List<File> mDirectories;
     64     private File mCurrentFile;
     65     private boolean mListAdvanced;
     66 
     67     /** Map from view id to map from attribute to frequency count */
     68     private Map<String, Map<String, Usage>> mFrequencies =
     69             new HashMap<String, Map<String, Usage>>(100);
     70 
     71     private Map<String, Map<String, Usage>> mLayoutAttributeFrequencies =
     72             new HashMap<String, Map<String, Usage>>(100);
     73 
     74     private Map<String, String> mTopAttributes = new HashMap<String, String>(100);
     75     private Map<String, String> mTopLayoutAttributes = new HashMap<String, String>(100);
     76 
     77     private int mFileVisitCount;
     78     private int mLayoutFileCount;
     79     private File mXmlMetadataFile;
     80 
     81     private Analyzer(List<File> directories, File xmlMetadataFile, boolean listAdvanced) {
     82         mDirectories = directories;
     83         mXmlMetadataFile = xmlMetadataFile;
     84         mListAdvanced = listAdvanced;
     85     }
     86 
     87     public static void main(String[] args) {
     88         if (args.length < 1) {
     89             System.err.println("Usage: " + Analyzer.class.getSimpleName()
     90                     + " <directory1> [directory2 [directory3 ...]]\n");
     91             System.err.println("Recursively scans for layouts in the given directory and");
     92             System.err.println("computes statistics about attribute frequencies.");
     93             System.exit(-1);
     94         }
     95 
     96         File metadataFile = null;
     97         List<File> directories = new ArrayList<File>();
     98         boolean listAdvanced = false;
     99         for (int i = 0, n = args.length; i < n; i++) {
    100             String arg = args[i];
    101 
    102             if (arg.equals("--list")) {
    103                 // List ALL encountered attributes
    104                 listAdvanced = true;
    105                 continue;
    106             }
    107 
    108             // The -metadata flag takes a pointer to an ADT extra-view-metadata.xml file
    109             // and attempts to insert topAttrs attributes into it (and saves it as same
    110             // file +.mod as an extension). This isn't listed on the usage flag because
    111             // it's pretty brittle and requires some manual fixups to the file afterwards.
    112             if (arg.equals("--metadata")) {
    113                 i++;
    114                 File file = new File(args[i]);
    115                 if (!file.exists()) {
    116                     System.err.println(file.getName() + " does not exist");
    117                     System.exit(-5);
    118                 }
    119                 if (!file.isFile() || !file.getName().endsWith(".xml")) {
    120                     System.err.println(file.getName() + " must be an XML file");
    121                     System.exit(-4);
    122                 }
    123                 metadataFile = file;
    124                 continue;
    125             }
    126             File directory = new File(arg);
    127             if (!directory.exists()) {
    128                 System.err.println(directory.getName() + " does not exist");
    129                 System.exit(-2);
    130             }
    131 
    132             if (!directory.isDirectory()) {
    133                 System.err.println(directory.getName() + " is not a directory");
    134                 System.exit(-3);
    135             }
    136 
    137             directories.add(directory);
    138         }
    139 
    140         new Analyzer(directories, metadataFile, listAdvanced).analyze();
    141     }
    142 
    143     private void analyze() {
    144         for (File directory : mDirectories) {
    145             scanDirectory(directory);
    146         }
    147 
    148         if (mListAdvanced) {
    149             listAdvanced();
    150         }
    151 
    152         printStatistics();
    153 
    154         if (mXmlMetadataFile != null) {
    155             printMergedMetadata();
    156         }
    157     }
    158 
    159     private void scanDirectory(File directory) {
    160         File[] files = directory.listFiles();
    161         if (files == null) {
    162             return;
    163         }
    164 
    165         for (File file : files) {
    166             mFileVisitCount++;
    167             if (mFileVisitCount % 50000 == 0) {
    168                 System.out.println("Analyzed " + mFileVisitCount + " files...");
    169             }
    170 
    171             if (file.isFile()) {
    172                 scanFile(file);
    173             } else if (file.isDirectory()) {
    174                 // Skip stuff related to tests
    175                 if (file.getName().contains("test")) {
    176                     continue;
    177                 }
    178 
    179                 // Recurse over subdirectories
    180                 scanDirectory(file);
    181             }
    182         }
    183     }
    184 
    185     private void scanFile(File file) {
    186         if (file.getName().endsWith(".xml")) {
    187             File parent = file.getParentFile();
    188             if (parent.getName().startsWith("layout")) {
    189                 analyzeLayout(file);
    190             }
    191         }
    192 
    193     }
    194 
    195     private void analyzeLayout(File file) {
    196         mCurrentFile = file;
    197         mLayoutFileCount++;
    198         Document document = null;
    199         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    200         InputSource is = new InputSource(new StringReader(readFile(file)));
    201         try {
    202             factory.setNamespaceAware(true);
    203             factory.setValidating(false);
    204             DocumentBuilder builder = factory.newDocumentBuilder();
    205             document = builder.parse(is);
    206 
    207             analyzeDocument(document);
    208 
    209         } catch (ParserConfigurationException e) {
    210             // pass -- ignore files we can't parse
    211         } catch (SAXException e) {
    212             // pass -- ignore files we can't parse
    213         } catch (IOException e) {
    214             // pass -- ignore files we can't parse
    215         }
    216     }
    217 
    218 
    219     private void analyzeDocument(Document document) {
    220         analyzeElement(document.getDocumentElement());
    221     }
    222 
    223     private void analyzeElement(Element element) {
    224         if (element.getTagName().equals("item")) {
    225             // Resource files shouldn't be in the layout/ folder but I came across
    226             // some cases
    227             System.out.println("Warning: found <item> tag in a layout file in "
    228                     + mCurrentFile.getPath());
    229             return;
    230         }
    231 
    232         countAttributes(element);
    233         countLayoutAttributes(element);
    234 
    235         // Recurse over children
    236         NodeList childNodes = element.getChildNodes();
    237         for (int i = 0, n = childNodes.getLength(); i < n; i++) {
    238             Node child = childNodes.item(i);
    239             if (child.getNodeType() == Node.ELEMENT_NODE) {
    240                 analyzeElement((Element) child);
    241             }
    242         }
    243     }
    244 
    245     private void countAttributes(Element element) {
    246         String tag = element.getTagName();
    247         Map<String, Usage> attributeMap = mFrequencies.get(tag);
    248         if (attributeMap == null) {
    249             attributeMap = new HashMap<String, Usage>(70);
    250             mFrequencies.put(tag, attributeMap);
    251         }
    252 
    253         NamedNodeMap attributes = element.getAttributes();
    254         for (int i = 0, n = attributes.getLength(); i < n; i++) {
    255             Node attribute = attributes.item(i);
    256             String name = attribute.getNodeName();
    257 
    258             if (name.startsWith("android:layout_")) {
    259                 // Skip layout attributes; they are a function of the parent layout that this
    260                 // view is embedded within, not the view itself.
    261                 // TODO: Consider whether we should incorporate this info or make statistics
    262                 // about that as well?
    263                 continue;
    264             }
    265 
    266             if (name.equals("android:id")) {
    267                 // Skip ids: they are (mostly) unrelated to the view type and the tool
    268                 // already offers id editing prominently
    269                 continue;
    270             }
    271 
    272             if (name.startsWith("xmlns:")) {
    273                 // Unrelated to frequency counts
    274                 continue;
    275             }
    276 
    277             Usage usage = attributeMap.get(name);
    278             if (usage == null) {
    279                 usage = new Usage(name);
    280             } else {
    281                 usage.incrementCount();
    282             }
    283             attributeMap.put(name, usage);
    284         }
    285     }
    286 
    287     private void countLayoutAttributes(Element element) {
    288         String parentTag = element.getParentNode().getNodeName();
    289         Map<String, Usage> attributeMap = mLayoutAttributeFrequencies.get(parentTag);
    290         if (attributeMap == null) {
    291             attributeMap = new HashMap<String, Usage>(70);
    292             mLayoutAttributeFrequencies.put(parentTag, attributeMap);
    293         }
    294 
    295         NamedNodeMap attributes = element.getAttributes();
    296         for (int i = 0, n = attributes.getLength(); i < n; i++) {
    297             Node attribute = attributes.item(i);
    298             String name = attribute.getNodeName();
    299 
    300             if (!name.startsWith("android:layout_")) {
    301                 continue;
    302             }
    303 
    304             // Skip layout_width and layout_height; they are mandatory in all but GridLayout so not
    305             // very interesting
    306             if (name.equals("android:layout_width") || name.equals("android:layout_height")) {
    307                 continue;
    308             }
    309 
    310             Usage usage = attributeMap.get(name);
    311             if (usage == null) {
    312                 usage = new Usage(name);
    313             } else {
    314                 usage.incrementCount();
    315             }
    316             attributeMap.put(name, usage);
    317         }
    318     }
    319 
    320     // Copied from AdtUtils
    321     private static String readFile(File file) {
    322         try {
    323             return readFile(new FileReader(file));
    324         } catch (FileNotFoundException e) {
    325             e.printStackTrace();
    326         }
    327 
    328         return null;
    329     }
    330 
    331     private static String readFile(Reader inputStream) {
    332         BufferedReader reader = null;
    333         try {
    334             reader = new BufferedReader(inputStream);
    335             StringBuilder sb = new StringBuilder(2000);
    336             while (true) {
    337                 int c = reader.read();
    338                 if (c == -1) {
    339                     return sb.toString();
    340                 } else {
    341                     sb.append((char)c);
    342                 }
    343             }
    344         } catch (IOException e) {
    345             // pass -- ignore files we can't read
    346         } finally {
    347             try {
    348                 if (reader != null) {
    349                     reader.close();
    350                 }
    351             } catch (IOException e) {
    352                 e.printStackTrace();
    353             }
    354         }
    355 
    356         return null;
    357     }
    358 
    359     private void printStatistics() {
    360         System.out.println("Analyzed " + mLayoutFileCount
    361                 + " layouts (in a directory trees containing " + mFileVisitCount + " files)");
    362         System.out.println("Top " + ATTRIBUTE_COUNT
    363                 + " for each view (excluding layout_ attributes) :");
    364         System.out.println("\n");
    365         System.out.println(" Rank    Count    Share  Attribute");
    366         System.out.println("=========================================================");
    367         List<String> views = new ArrayList<String>(mFrequencies.keySet());
    368         Collections.sort(views);
    369         for (String view : views) {
    370             String top = processUageMap(view, mFrequencies.get(view));
    371             if (top != null) {
    372                 mTopAttributes.put(view,  top);
    373             }
    374         }
    375 
    376         System.out.println("\n\n\nTop " + ATTRIBUTE_COUNT + " layout attributes (excluding "
    377                 + "mandatory layout_width and layout_height):");
    378         System.out.println("\n");
    379         System.out.println(" Rank    Count    Share  Attribute");
    380         System.out.println("=========================================================");
    381         views = new ArrayList<String>(mLayoutAttributeFrequencies.keySet());
    382         Collections.sort(views);
    383         for (String view : views) {
    384             String top = processUageMap(view, mLayoutAttributeFrequencies.get(view));
    385             if (top != null) {
    386                 mTopLayoutAttributes.put(view,  top);
    387             }
    388         }
    389     }
    390 
    391     private static String processUageMap(String view, Map<String, Usage> map) {
    392         if (map == null) {
    393             return null;
    394         }
    395 
    396         if (view.indexOf('.') != -1 && !view.startsWith("android.")) {
    397             // Skip custom views
    398             return null;
    399         }
    400 
    401         List<Usage> values = new ArrayList<Usage>(map.values());
    402         if (values.size() == 0) {
    403             return null;
    404         }
    405 
    406         Collections.sort(values);
    407         int totalCount = 0;
    408         for (Usage usage : values) {
    409             totalCount += usage.count;
    410         }
    411 
    412         System.out.println("\n<" + view + ">:");
    413         if (view.equals("#document")) {
    414             System.out.println("(Set on root tag, probably intended for included context)");
    415         }
    416 
    417         int place = 1;
    418         int count = 0;
    419         int prevCount = -1;
    420         float prevPercentage = 0f;
    421         StringBuilder sb = new StringBuilder();
    422         for (Usage usage : values) {
    423             if (count++ >= ATTRIBUTE_COUNT && usage.count < prevCount) {
    424                 break;
    425             }
    426 
    427             float percentage = 100 * usage.count/(float)totalCount;
    428             if (percentage < THRESHOLD && prevPercentage >= THRESHOLD) {
    429                 System.out.println("  -----Less than 10%-------------------------------------");
    430             }
    431             System.out.printf("  %1d.    %5d    %5.1f%%  %s\n", place, usage.count,
    432                     percentage, usage.attribute);
    433 
    434             prevPercentage = percentage;
    435             if (prevCount != usage.count) {
    436                 prevCount = usage.count;
    437                 place++;
    438             }
    439 
    440             if (percentage >= THRESHOLD /*&& usage.count > 1*/) { // 1:Ignore when not enough data?
    441                 if (sb.length() > 0) {
    442                     sb.append(',');
    443                 }
    444                 String name = usage.attribute;
    445                 if (name.startsWith("android:")) {
    446                     name = name.substring("android:".length());
    447                 }
    448                 sb.append(name);
    449             }
    450         }
    451 
    452         return sb.length() > 0 ? sb.toString() : null;
    453     }
    454 
    455     private void printMergedMetadata() {
    456         assert mXmlMetadataFile != null;
    457         String metadata = readFile(mXmlMetadataFile);
    458         if (metadata == null || metadata.length() == 0) {
    459             System.err.println("Invalid metadata file");
    460             System.exit(-6);
    461         }
    462 
    463         System.err.flush();
    464         System.out.println("\n\nUpdating layout metadata file...");
    465         System.out.flush();
    466 
    467         StringBuilder sb = new StringBuilder((int) (2 * mXmlMetadataFile.length()));
    468         String[] lines = metadata.split("\n");
    469         for (int i = 0; i < lines.length; i++) {
    470             String line = lines[i];
    471             sb.append(line).append('\n');
    472             int classIndex = line.indexOf("class=\"");
    473             if (classIndex != -1) {
    474                 int start = classIndex + "class=\"".length();
    475                 int end = line.indexOf('"', start + 1);
    476                 if (end != -1) {
    477                     String view = line.substring(start, end);
    478                     if (view.startsWith("android.widget.")) {
    479                         view = view.substring("android.widget.".length());
    480                     } else if (view.startsWith("android.view.")) {
    481                         view = view.substring("android.view.".length());
    482                     } else if (view.startsWith("android.webkit.")) {
    483                         view = view.substring("android.webkit.".length());
    484                     }
    485                     String top = mTopAttributes.get(view);
    486                     if (top == null) {
    487                         System.err.println("Warning: No frequency data for view " + view);
    488                     } else {
    489                         sb.append(line.substring(0, classIndex)); // Indentation
    490 
    491                         sb.append("topAttrs=\"");
    492                         sb.append(top);
    493                         sb.append("\"\n");
    494                     }
    495 
    496                     top = mTopLayoutAttributes.get(view);
    497                     if (top != null) {
    498                         // It's a layout attribute
    499                         sb.append(line.substring(0, classIndex)); // Indentation
    500 
    501                         sb.append("topLayoutAttrs=\"");
    502                         sb.append(top);
    503                         sb.append("\"\n");
    504                     }
    505                 }
    506             }
    507         }
    508 
    509         System.out.println("\nTop attributes:");
    510         System.out.println("--------------------------");
    511         List<String> views = new ArrayList<String>(mTopAttributes.keySet());
    512         Collections.sort(views);
    513         for (String view : views) {
    514             String top = mTopAttributes.get(view);
    515             System.out.println(view + ": " + top);
    516         }
    517 
    518         System.out.println("\nTop layout attributes:");
    519         System.out.println("--------------------------");
    520         views = new ArrayList<String>(mTopLayoutAttributes.keySet());
    521         Collections.sort(views);
    522         for (String view : views) {
    523             String top = mTopLayoutAttributes.get(view);
    524             System.out.println(view + ": " + top);
    525         }
    526 
    527         System.out.println("\nModified XML metadata file:\n");
    528         String newContent = sb.toString();
    529         File output = new File(mXmlMetadataFile.getParentFile(), mXmlMetadataFile.getName() + ".mod");
    530         if (output.exists()) {
    531             output.delete();
    532         }
    533         try {
    534             BufferedWriter writer = new BufferedWriter(new FileWriter(output));
    535             writer.write(newContent);
    536             writer.close();
    537         } catch (IOException e) {
    538             e.printStackTrace();
    539         }
    540         System.out.println("Done - wrote " + output.getPath());
    541     }
    542 
    543     //private File mPublicFile = new File(location, "data/res/values/public.xml");
    544     private File mPublicFile = new File("/Volumes/AndroidWork/git/frameworks/base/core/res/res/values/public.xml");
    545 
    546     private void listAdvanced() {
    547         Set<String> keys = new HashSet<String>(1000);
    548 
    549         // Merged usages across view types
    550         Map<String, Usage> mergedUsages = new HashMap<String, Usage>(100);
    551 
    552         for (Entry<String,Map<String,Usage>> entry : mFrequencies.entrySet()) {
    553             String view = entry.getKey();
    554             if (view.indexOf('.') != -1 && !view.startsWith("android.")) {
    555                 // Skip custom views etc
    556                 continue;
    557             }
    558             Map<String, Usage> map = entry.getValue();
    559             for (Usage usage : map.values()) {
    560 //                if (usage.count == 1) {
    561 //                    System.out.println("Only found *one* usage of " + usage.attribute);
    562 //                }
    563 //                if (usage.count < 4) {
    564 //                    System.out.println("Only found " + usage.count + " usage of " + usage.attribute);
    565 //                }
    566 
    567                 String attribute = usage.attribute;
    568                 int index = attribute.indexOf(':');
    569                 if (index == -1 || attribute.startsWith("android:")) {
    570                     Usage merged = mergedUsages.get(attribute);
    571                     if (merged == null) {
    572                         merged = new Usage(attribute);
    573                         merged.count = usage.count;
    574                         mergedUsages.put(attribute, merged);
    575                     } else {
    576                         merged.count += usage.count;
    577                     }
    578                 }
    579             }
    580         }
    581 
    582         for (Usage usage : mergedUsages.values())  {
    583             String attribute = usage.attribute;
    584             if (usage.count < 4) {
    585                 System.out.println("Only found " + usage.count + " usage of " + usage.attribute);
    586                 continue;
    587             }
    588             int index = attribute.indexOf(':');
    589             if (index != -1) {
    590                 attribute = attribute.substring(index + 1); // +1: skip ':'
    591             }
    592             keys.add(attribute);
    593         }
    594 
    595         List<String> sorted = new ArrayList<String>(keys);
    596         Collections.sort(sorted);
    597         System.out.println("\nEncountered Attributes");
    598         System.out.println("-----------------------------");
    599         for (String attribute : sorted) {
    600             System.out.println(attribute);
    601         }
    602 
    603         System.out.println();
    604     }
    605 
    606     private static class Usage implements Comparable<Usage> {
    607         public String attribute;
    608         public int count;
    609 
    610 
    611         public Usage(String attribute) {
    612             super();
    613             this.attribute = attribute;
    614 
    615             count = 1;
    616         }
    617 
    618         public void incrementCount() {
    619             count++;
    620         }
    621 
    622         @Override
    623         public int compareTo(Usage o) {
    624             // Sort by decreasing frequency, then sort alphabetically
    625             int frequencyDelta = o.count - count;
    626             if (frequencyDelta != 0) {
    627                 return frequencyDelta;
    628             } else {
    629                 return attribute.compareTo(o.attribute);
    630             }
    631         }
    632 
    633         @Override
    634         public String toString() {
    635             return attribute + ": " + count;
    636         }
    637 
    638         @Override
    639         public int hashCode() {
    640             final int prime = 31;
    641             int result = 1;
    642             result = prime * result + ((attribute == null) ? 0 : attribute.hashCode());
    643             return result;
    644         }
    645 
    646         @Override
    647         public boolean equals(Object obj) {
    648             if (this == obj)
    649                 return true;
    650             if (obj == null)
    651                 return false;
    652             if (getClass() != obj.getClass())
    653                 return false;
    654             Usage other = (Usage) obj;
    655             if (attribute == null) {
    656                 if (other.attribute != null)
    657                     return false;
    658             } else if (!attribute.equals(other.attribute))
    659                 return false;
    660             return true;
    661         }
    662     }
    663 }
    664