| #!/bin/sh | |
| # Copyright 2015 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| # Check if the stateful partition needs to be trimmed. | |
| # Trim is triggered if: | |
| # - Previous run was interrupted, in case of reboot or heavy disk usage. | |
| # - The amount of write since the last trim is greater the X% of remaining | |
| # space in stateful. | |
| # - Not trimmed for a long time. | |
| # | |
| # When trim is triggered, we constantly check that the underlying device is | |
| # not heavily used. | |
| . /usr/share/misc/shflags | |
| . /usr/share/cros/disk_utils.sh | |
| DEFINE_boolean 'test' ${FLAGS_FALSE} "For unit testing." | |
| # Parse command line. | |
| FLAGS "$@" || exit 1 | |
| eval set -- "${FLAGS_ARGV}" | |
| set -e | |
| if [ "${FLAGS_test}" -eq "${FLAGS_FALSE}" ]; then | |
| . /usr/share/misc/chromeos-common.sh | |
| . /usr/share/cros/trim_utils.sh | |
| # Names of device and sysfs entries the script will work with. | |
| # These variables are mocked by the test harness. | |
| PARTITION_DEV="$(get_stateful_df_data | awk '{print $1}' | sed -e 's#/dev/##')" | |
| DEVICE="$(get_block_dev_from_partition_dev "${PARTITION_DEV}")" | |
| DEVICE_STATS="/sys/block/${DEVICE}/stat" | |
| PARTITION_DEV_STATS="/sys/block/${DEVICE}/${PARTITION_DEV}/stat" | |
| else | |
| . ./share/trim_utils.sh | |
| fi | |
| # Maximal amount of time before forcing a trim. | |
| MAX_DELAY_DAYS=7 | |
| MAX_DELAY_HOURS=$(( MAX_DELAY_DAYS * 24 )) | |
| MAX_DELAY=$(( MAX_DELAY_DAYS * 24 * 3600 )) | |
| # Ratio of write/free space to trigger a trim. | |
| MAX_TOTAL_WRITE_PERCENT=75 | |
| # Slice of disk processed by each fstrim call. | |
| FSTRIM_SLICE_MB=128 | |
| # Maximum amount of time to spend in this script before rescheduling. | |
| MAX_TRIM_WORKER_TIME_SEC=3000 | |
| # Time between fstrim calls when the disk is in steady state. | |
| DELAY_BTW_FSTRIM_SEC=5 | |
| # Time to wait if disk is heavily used. | |
| DELAY_HIGH_USAGE_SEC=15 | |
| # Maximal disk utilization for fstrim to proceed. | |
| MAX_DISK_UTIL_PERCENT=20 | |
| # Maximal amount in milliseconds of disk utilization to still trigger trim. | |
| MAX_DISK_UTIL_MS=$(( DELAY_BTW_FSTRIM_SEC * 1000 * \ | |
| MAX_DISK_UTIL_PERCENT / 100 )) | |
| # Global variables: | |
| # current_offset : current_offset to use when TRIM process resume, | |
| # duration : Amount of time spent trimming so far, | |
| # write_last_trim : Amount of data written when last TRIM completed, | |
| # date_last_trim : When the last TRIM completed, | |
| # trimmed_amount : Amount of data trimmed by the last TRIM process, | |
| # trim_status : Status of the current/last TRIM process. | |
| log_msg() { | |
| logger -t "$0[${PPID}]" "$*" | |
| echo "$*" | |
| } | |
| load_int() { | |
| local var="$1" | |
| local val | |
| if [ -f "${CHROMEOS_TRIM_DATA}" ]; then | |
| # Make sure we only permit the value to be set to an integer. | |
| val=$(sed -n "/^${var}=/{s:.*=::;s:[^0-9]::g;p;q}" "${CHROMEOS_TRIM_DATA}") | |
| fi | |
| eval "${var}=${val:-0}" | |
| } | |
| # Read status file, to know current process, Populate global variables. | |
| read_status_file() { | |
| load_int current_offset | |
| load_int duration | |
| load_int write_last_trim | |
| load_int date_last_trim | |
| load_int trimmed_amount | |
| load_int stateful_writes | |
| trim_status="$(cat "${CHROMEOS_TRIM_STATE}" 2> /dev/null || : )" | |
| } | |
| write_status_file() { | |
| cat > "${CHROMEOS_TRIM_DATA}" << EOF | |
| # Automatically generated by $0 | |
| current_offset=${current_offset} | |
| duration=${duration} | |
| write_last_trim=${write_last_trim} | |
| date_last_trim=${date_last_trim} | |
| trimmed_amount=${trimmed_amount} | |
| stateful_writes=${stateful_writes} | |
| EOF | |
| echo "${trim_status}" > "${CHROMEOS_TRIM_STATE}" | |
| } | |
| cleanup() { | |
| write_status_file | |
| exit 0 | |
| } | |
| # Return the number of ms spend doing disk io since boot. | |
| get_disk_time_ms() { | |
| awk '{print $10}' "${DEVICE_STATS}" | |
| } | |
| # Trim the stateful partition. | |
| # | |
| # Trim a slice of the partition, sleep a little to be sure | |
| # we are not in the middle of heavy disk traffic. | |
| # Wait a little longer if we are, and bails if the process takes | |
| # too long. | |
| do_trim() { | |
| local partition_size | |
| partition_size="$(get_stateful_total_space_blocks "1M")" | |
| local start_time | |
| start_time="$(date +%s)" | |
| local current_duration=0 | |
| local before_time_spent_io_ms | |
| local after_time_spent_io_ms | |
| local output | |
| local trimmed | |
| local trim_length=${FSTRIM_SLICE_MB} | |
| local time_between_trim | |
| local time_between_hours | |
| local trimmed_amount_mb | |
| read_status_file | |
| trim_status="${TRIM_IN_PROGRESS}" | |
| : $(( time_between_trim = start_time - date_last_trim )) | |
| if [ "${current_offset}" -eq 0 ]; then | |
| trimmed_amount=0 | |
| duration=0 | |
| fi | |
| while [ "${current_offset}" -lt "${partition_size}" ] && \ | |
| [ "${current_duration}" -lt "${MAX_TRIM_WORKER_TIME_SEC}" ]; do | |
| before_time_spent_io_ms=$(get_disk_time_ms) | |
| sleep "${DELAY_BTW_FSTRIM_SEC}" | |
| after_time_spent_io_ms=$(get_disk_time_ms) | |
| if [ $(( after_time_spent_io_ms - before_time_spent_io_ms )) -gt \ | |
| ${MAX_DISK_UTIL_MS} ]; then | |
| sleep "${DELAY_HIGH_USAGE_SEC}" | |
| else | |
| if [ $(( partition_size - current_offset )) -le ${FSTRIM_SLICE_MB} ]; then | |
| : $(( trim_length = partition_size - current_offset )) | |
| fi | |
| if ! output=$(fstrim --verbose --offset "${current_offset}M" \ | |
| --length "${trim_length}M" "${STATEFUL}"); then | |
| log_msg "Error Trimming ${STATEFUL} at" \ | |
| "current_offset ${current_offset}M: ${output}" | |
| trim_status="${TRIM_FAILED}" | |
| break | |
| fi | |
| trimmed=$(echo "${output}" | sed -ne 's/.*(\([0-9]\+\) bytes) trimmed.*/\1/p') | |
| if [ -z "${trimmed}" ]; then | |
| log_msg "Unexpected fstrim output at" \ | |
| "current_offset ${current_offset}M : ${output}" | |
| trim_status="${TRIM_FAILED}" | |
| break | |
| fi | |
| : $(( trimmed_amount += trimmed )) | |
| : $(( current_offset += trim_length )) | |
| fi | |
| date_last_trim=$(date +%s) | |
| : $(( current_duration = date_last_trim - start_time )) | |
| done | |
| : $(( trimmed_amount_mb = trimmed_amount / 1048576 )) | |
| metrics_client Platform.StatefulTrim.TrimmedAmount "${trimmed_amount_mb}" \ | |
| 0 10240 50 | |
| : $(( time_between_hours = time_between_trim / 3600 )) | |
| metrics_client Platform.StatefulTrim.TimeBetweenTrim "${time_between_hours}" \ | |
| 0 ${MAX_DELAY_HOURS} 20 | |
| : $(( duration += current_duration )) | |
| # Prepare to save status in status file. | |
| if [ "${current_offset}" -eq "${partition_size}" ]; then | |
| current_offset=0 | |
| write_last_trim="$(awk '{print $7}' "${PARTITION_DEV_STATS}")" | |
| trim_status="${TRIM_COMPLETED}" | |
| log_msg "${STATEFUL} fully trimmed in ${duration} seconds" | |
| else | |
| log_msg "${STATEFUL} trim interrupted at ${current_offset}M out of" \ | |
| "${partition_size}M, ${duration} seconds so far" | |
| fi | |
| } | |
| get_device_block_size() { | |
| cat "/sys/block/${DEVICE}/queue/logical_block_size" | |
| } | |
| # Checks if the SSD needs to be trimmed. | |
| main() { | |
| local trigger_trim=false | |
| local block_size | |
| local free_blocks | |
| local written_blocks | |
| local current_stateful_writes | |
| local stateful_diff | |
| if [ -f "${CHROMEOS_TRIM_DATA}" ]; then | |
| read_status_file | |
| current_stateful_writes="$(get_stateful_lifetime_writes)" | |
| if [ "${stateful_writes}" -ne 0 ]; then | |
| stateful_diff="$((${current_stateful_writes} - ${stateful_writes}))" | |
| # The UMA metric for stateful lifetime writes has the 99th | |
| # percentile at 4,800 GiB. It seems reasonable to set the max | |
| # for the daily metric at 200 GiB. | |
| metrics_client Platform.StatefulWritesDaily \ | |
| "${stateful_diff}" 0 209715200 50 | |
| fi | |
| stateful_writes="${current_stateful_writes}" | |
| if [ "${current_offset}" -ne 0 ]; then | |
| # Trim was interrupted due to intense disk activity, resume. | |
| trigger_trim=true | |
| fi | |
| # Last trim was completed a long time ago. | |
| if [ "$(( date_last_trim + MAX_DELAY ))" -lt "$(date +%s)" ]; then | |
| trigger_trim=true | |
| fi | |
| else | |
| # Status file not present, trim. | |
| trigger_trim=true | |
| fi | |
| if ! ${trigger_trim}; then | |
| block_size="$(get_device_block_size)" | |
| free_blocks="$(get_stateful_free_space_blocks "${block_size}")" | |
| written_blocks=$(awk '{print $7}' "${PARTITION_DEV_STATS}") | |
| if [ "$(date +%s -d "$(uptime -s)")" -gt "${date_last_trim}" ]; then | |
| # Machine has rebooted since last trim, the amount of writes since | |
| # last trim is irrelevant. | |
| write_last_trim=0 | |
| fi | |
| : $(( written_blocks -= write_last_trim )) | |
| # Trigger Trim if at least the amount of write since boot is more than | |
| # 75% of the free blocks. | |
| if [ $(( written_blocks * 100 )) -gt \ | |
| $(( free_blocks * MAX_TOTAL_WRITE_PERCENT )) ]; then | |
| trigger_trim=true | |
| fi | |
| fi | |
| if "${trigger_trim}"; then | |
| do_trim | |
| fi | |
| } | |
| # Invoke main if not in test mode, otherwise let the test code call. | |
| if [ "${FLAGS_test}" -eq "${FLAGS_FALSE}" ]; then | |
| ( | |
| if ! flock -n 9; then | |
| log_msg "Already running; quitting." | |
| exit 1 | |
| fi | |
| # In case trim is interrupt by regular shutdown, save the current status. | |
| trap cleanup EXIT INT TERM | |
| main "$@" | |
| ) 9>"${CHROMEOS_TRIM_LOCK}" | |
| fi |