| # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import os |
| import re |
| |
| import throttler_lib |
| import utils_lib |
| |
| |
| # File extensions that can not be shrunk., as partial content will corrupt the |
| # file. |
| UNSHRINKABLE_EXTENSIONS = set([ |
| '.bin', |
| '.data', |
| '.dmp', |
| '.gz', |
| '.htm', |
| '.html', |
| '.img', |
| '.jpg', |
| '.json', |
| '.png', |
| '.tar', |
| '.tgz', |
| '.xml', |
| '.xz', |
| '.zip', |
| ]) |
| |
| # Regex for files that should not be shrunk. |
| UNSHRINKABLE_FILE_PATTERNS = [ |
| ] |
| |
| TRIMMED_FILE_HEADER = '!!! This file is trimmed !!!\n' |
| ORIGINAL_SIZE_TEMPLATE = 'Original size: %d bytes\n\n' |
| # Regex pattern to retrieve the original size of the file. |
| ORIGINAL_SIZE_REGEX = 'Original size: (\d+) bytes' |
| TRIMMED_FILE_INJECT_TEMPLATE = """ |
| |
| ======================================================================== |
| < %d > characters are trimmed here. |
| ======================================================================== |
| |
| """ |
| |
| # Percent of file content to keep at the beginning and end of the file, default |
| # to 20%. |
| HEAD_SIZE_PERCENT = 0.20 |
| |
| # Default size in byte to trim the file down to. |
| DEFAULT_FILE_SIZE_LIMIT_BYTE = 100 * 1024 |
| |
| def _trim_file(file_info, file_size_limit_byte): |
| """Remove the file content in the middle to reduce the file size. |
| |
| @param file_info: A ResultInfo object containing summary for the file to be |
| shrunk. |
| @param file_size_limit_byte: Maximum file size in bytes after trimming. |
| """ |
| utils_lib.LOG('Trimming file %s to reduce size from %d bytes to %d bytes' % |
| (file_info.path, file_info.original_size, |
| file_size_limit_byte)) |
| new_path = os.path.join(os.path.dirname(file_info.path), |
| file_info.name + '_trimmed') |
| original_size_bytes = file_info.original_size |
| with open(new_path, 'w') as new_file, open(file_info.path) as old_file: |
| # Read the beginning part of the old file, if it's already started with |
| # TRIMMED_FILE_HEADER, no need to add the header again. |
| header = old_file.read(len(TRIMMED_FILE_HEADER)) |
| if header != TRIMMED_FILE_HEADER: |
| new_file.write(TRIMMED_FILE_HEADER) |
| new_file.write(ORIGINAL_SIZE_TEMPLATE % file_info.original_size) |
| else: |
| line = old_file.readline() |
| match = re.match(ORIGINAL_SIZE_REGEX, line) |
| if match: |
| original_size_bytes = int(match.group(1)) |
| header_size_bytes = new_file.tell() |
| # Move old file reader to the beginning of the file. |
| old_file.seek(0, os.SEEK_SET) |
| |
| new_file.write(old_file.read( |
| int((file_size_limit_byte - header_size_bytes) * |
| HEAD_SIZE_PERCENT))) |
| # Position to seek from the end of the file. |
| seek_pos = -(file_size_limit_byte - new_file.tell() - |
| len(TRIMMED_FILE_INJECT_TEMPLATE)) |
| bytes_to_skip = original_size_bytes + seek_pos - old_file.tell() |
| # Adjust seek position based on string TRIMMED_FILE_INJECT_TEMPLATE |
| seek_pos += len(str(bytes_to_skip)) - 2 |
| bytes_to_skip = original_size_bytes + seek_pos - old_file.tell() |
| new_file.write(TRIMMED_FILE_INJECT_TEMPLATE % bytes_to_skip) |
| old_file.seek(seek_pos, os.SEEK_END) |
| new_file.write(old_file.read()) |
| stat = os.stat(file_info.path) |
| if not throttler_lib.try_delete_file_on_disk(file_info.path): |
| # Clean up the intermediate file. |
| throttler_lib.try_delete_file_on_disk(new_path) |
| utils_lib.LOG('Failed to shrink %s' % file_info.path) |
| return |
| |
| os.rename(new_path, file_info.path) |
| # Modify the new file's timestamp to the old one. |
| os.utime(file_info.path, (stat.st_atime, stat.st_mtime)) |
| # Update the trimmed_size. |
| file_info.trimmed_size = file_info.size |
| |
| |
| def _get_shrinkable_files(file_infos, file_size_limit_byte): |
| """Filter the files that can be throttled. |
| |
| @param file_infos: A list of ResultInfo objects. |
| @param file_size_limit_byte: Minimum file size in bytes to be throttled. |
| @yield: ResultInfo objects that can be shrunk. |
| """ |
| for info in file_infos: |
| ext = os.path.splitext(info.name)[1].lower() |
| if ext in UNSHRINKABLE_EXTENSIONS: |
| continue |
| |
| match_found = False |
| for pattern in UNSHRINKABLE_FILE_PATTERNS: |
| if re.match(pattern, info.name): |
| match_found = True |
| break |
| if match_found: |
| continue |
| |
| if info.trimmed_size <= file_size_limit_byte: |
| continue |
| |
| yield info |
| |
| |
| def throttle(summary, max_result_size_KB, |
| file_size_limit_byte=DEFAULT_FILE_SIZE_LIMIT_BYTE, |
| skip_autotest_log=False): |
| """Throttle the files in summary by trimming file content. |
| |
| Stop throttling until all files are processed or the result file size is |
| already reduced to be under the given max_result_size_KB. |
| |
| @param summary: A ResultInfo object containing result summary. |
| @param max_result_size_KB: Maximum test result size in KB. |
| @param file_size_limit_byte: Limit each file's size in the summary to be |
| under the given threshold, until all files are processed or the |
| result size is under the given max_result_size_KB. |
| @param skip_autotest_log: True to skip shrink Autotest logs, default is |
| False. |
| """ |
| file_infos, _ = throttler_lib.sort_result_files(summary) |
| extra_patterns = ([throttler_lib.AUTOTEST_LOG_PATTERN] if skip_autotest_log |
| else []) |
| file_infos = throttler_lib.get_throttleable_files( |
| file_infos, extra_patterns) |
| file_infos = _get_shrinkable_files(file_infos, file_size_limit_byte) |
| for info in file_infos: |
| _trim_file(info, file_size_limit_byte) |
| |
| if throttler_lib.check_throttle_limit(summary, max_result_size_KB): |
| return |