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 Analyzer(List<File> directories, File xmlMetadataFile, boolean listAdvanced)81 private Analyzer(List<File> directories, File xmlMetadataFile, boolean listAdvanced) { 82 mDirectories = directories; 83 mXmlMetadataFile = xmlMetadataFile; 84 mListAdvanced = listAdvanced; 85 } 86 main(String[] args)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 analyze()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 scanDirectory(File directory)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 scanFile(File file)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 analyzeLayout(File file)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 analyzeDocument(Document document)219 private void analyzeDocument(Document document) { 220 analyzeElement(document.getDocumentElement()); 221 } 222 analyzeElement(Element element)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 countAttributes(Element element)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 countLayoutAttributes(Element element)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 readFile(File file)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 readFile(Reader inputStream)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 printStatistics()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 processUageMap(String view, Map<String, Usage> map)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 printMergedMetadata()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 listAdvanced()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 Usage(String attribute)611 public Usage(String attribute) { 612 super(); 613 this.attribute = attribute; 614 615 count = 1; 616 } 617 incrementCount()618 public void incrementCount() { 619 count++; 620 } 621 622 @Override compareTo(Usage o)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 toString()634 public String toString() { 635 return attribute + ": " + count; 636 } 637 638 @Override hashCode()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 equals(Object obj)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