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