cleanup_stale_fieldtrial_configs.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. # Copyright 2021 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Simple script for cleaning up stale configs from fieldtrial_testing_config.
  5. Methodology:
  6. Scan for all study names that appear in fieldtrial config file,
  7. and removes ones that don't appear anywhere in the codebase.
  8. The script ignores WebRTC entries as those often lead to false positives.
  9. Usage:
  10. vpython tools/variations/cleanup_stale_fieldtrial_configs.py
  11. Run with --help to get a complete list of options this script runs with.
  12. If this script removes features that appear to be used in the codebase,
  13. double-check the study or feature name for typos or case differences.
  14. """
  15. from __future__ import print_function
  16. import json
  17. import optparse
  18. import os
  19. import subprocess
  20. import sys
  21. import threading
  22. CONFIG_PATH = 'testing/variations/fieldtrial_testing_config.json'
  23. PRESUBMIT_SCRIPT = 'testing/variations/PRESUBMIT.py'
  24. THREAD_COUNT = 16
  25. _LITERAL_CACHE = {}
  26. def is_literal_used(literal, code_files):
  27. """Check if a given string literal is used in the passed code files."""
  28. if literal in _LITERAL_CACHE:
  29. return _LITERAL_CACHE[literal]
  30. git_grep_cmd = ('git', 'grep', '--threads', '2', '-l', '\"%s\"' % literal)
  31. git_grep_proc = subprocess.Popen(git_grep_cmd, stdout=subprocess.PIPE)
  32. # Check for >1 since fieldtrial_testing_config.json will always be a result.
  33. if len(git_grep_proc.stdout.read().splitlines()) > 1:
  34. _LITERAL_CACHE[literal] = True
  35. return True
  36. bash_files_using_literal = subprocess.Popen(
  37. ('xargs', 'grep', '-s', '-l', '\\\"%s\\\"' % literal),
  38. stdin=subprocess.PIPE,
  39. stdout=subprocess.PIPE)
  40. files_using_literal = bash_files_using_literal.communicate(code_files)[0]
  41. used = len(files_using_literal.splitlines()) > 0
  42. _LITERAL_CACHE[literal] = used
  43. if not used:
  44. print('Did not find', repr(literal))
  45. return used
  46. def is_study_used(study_name, configs, code_files):
  47. """Checks if a given study is used in the codebase."""
  48. if study_name.startswith('WebRTC-'):
  49. return True # Skip webrtc studies which give false positives.
  50. if is_literal_used(study_name, code_files):
  51. return True
  52. for config in configs:
  53. for experiment in config.get('experiments', []):
  54. for feature in experiment.get('enable_features', []):
  55. if is_literal_used(feature, code_files):
  56. return True
  57. for feature in experiment.get('disable_features', []):
  58. if is_literal_used(feature, code_files):
  59. return True
  60. return False
  61. def thread_func(thread_limiter, studies_map, study_name, configs, code_files):
  62. """Runs a limited number of tasks and updates the map with the results.
  63. Args:
  64. thread_limited: A lock used to limit the number of active threads.
  65. studies_map: The map where confirmed studies are added to.
  66. study_name: The name of the study to check.
  67. configs: The configs for the given study.
  68. code_files: A string with the paths to all code files (cc or h files).
  69. Side-effect:
  70. This function adds the study to |studies_map| if it used.
  71. """
  72. thread_limiter.acquire()
  73. try:
  74. if is_study_used(study_name, configs, code_files):
  75. studies_map[study_name] = configs
  76. finally:
  77. thread_limiter.release()
  78. def main():
  79. parser = optparse.OptionParser()
  80. parser.add_option('--input_path',
  81. help='Path to the fieldtrial config file to clean.')
  82. parser.add_option('--output_path',
  83. help='Path to write cleaned up fieldtrial config file.')
  84. parser.add_option('--thread_count',
  85. type='int',
  86. help='The number of threads to use for scanning.')
  87. opts, _ = parser.parse_args()
  88. input_path = os.path.expanduser(opts.input_path or CONFIG_PATH)
  89. output_path = os.path.expanduser(opts.output_path or CONFIG_PATH)
  90. thread_limiter = threading.BoundedSemaphore(opts.thread_count or THREAD_COUNT)
  91. with open(input_path) as fin:
  92. studies = json.load(fin)
  93. print('Loaded config from', input_path)
  94. bash_list_files = subprocess.Popen(['find', '.', '-type', 'f'],
  95. stdout=subprocess.PIPE)
  96. bash_code_files = subprocess.Popen(['grep', '-E', '\\.(h|cc)$'],
  97. stdin=bash_list_files.stdout,
  98. stdout=subprocess.PIPE)
  99. bash_filtered_code_files = subprocess.Popen(
  100. ('grep', '-Ev', '(/out/|/build/|/gen/)'),
  101. stdin=bash_code_files.stdout,
  102. stdout=subprocess.PIPE)
  103. filtered_code_files = bash_filtered_code_files.stdout.read()
  104. threads = []
  105. clean_studies = {}
  106. for study_name, configs in studies.items():
  107. args = (thread_limiter, clean_studies, study_name, configs,
  108. filtered_code_files)
  109. threads.append(threading.Thread(target=thread_func, args=args))
  110. # Start all threads, then join all threads.
  111. for t in threads:
  112. t.start()
  113. for t in threads:
  114. t.join()
  115. with open(output_path, 'wt') as fout:
  116. json.dump(clean_studies, fout)
  117. print('Wrote cleaned config to', output_path)
  118. # Run presubmit script to format config file.
  119. retcode = subprocess.call(['vpython', PRESUBMIT_SCRIPT, output_path])
  120. if retcode != 0:
  121. print('Failed to format output, manually run:')
  122. print('vpython', PRESUBMIT_SCRIPT, output_path)
  123. if __name__ == '__main__':
  124. sys.exit(main())