blob: 13f08bf72540f88818d1d05ca18a01dd1e3b50af [file] [log] [blame]
#!/bin/sh
# Copyright 2015 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.
# 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