From 1f0928c4804cd3365913ccfa2065e70d2e07c1e1 Mon Sep 17 00:00:00 2001 From: zhaoyingzhen Date: Thu, 16 Apr 2026 14:41:43 +0800 Subject: [PATCH] feat: add dde session boot time and memory analysis tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a professional boot time analysis script for DDE session. - Displays systemd user service timelines and critical paths. - Provides reliable systemd-analyze blame data by filtering notification/dbus types. - Lists session processes with memory usage and relative start times. - Analyzes dde-shell and desktop logs for internal bottlenecks. - Validates systemd service configurations and gives optimization advice. 新增 DDE 会话启动耗时及内存分析专业脚本。 - 显示 systemd 用户服务启动时间线和关键路径耗时。 - 通过过滤 notification/dbus 类型提供可靠的 systemd-analyze blame 数据。 - 列出会话进程及其内存占用和相对启动时刻。 - 分析 dde-shell 和桌面日志以识别内部瓶颈。 - 验证 systemd 服务配置并提供具体的优化建议。 Log: add dde session boot time analysis tool Pms: TASK-384099 Change-Id: Ib6fa8374eacc3c93822c1c17b3301386d42c92d9 --- tools/analyze-boot-time.sh | 631 +++++++++++++++++++++++++++++++++++++ tools/analyze-memory.sh | 347 ++++++++++++++++++++ 2 files changed, 978 insertions(+) create mode 100755 tools/analyze-boot-time.sh create mode 100755 tools/analyze-memory.sh diff --git a/tools/analyze-boot-time.sh b/tools/analyze-boot-time.sh new file mode 100755 index 0000000..9f20c1e --- /dev/null +++ b/tools/analyze-boot-time.sh @@ -0,0 +1,631 @@ +#!/bin/bash +# DDE 启动性能分析脚本 +# 用法:./analyze-boot-time.sh [--report] +# 输出:systemd 用户会话启动时间线、关键服务耗时、优化建议 +# +# 选项: +# --report 生成 Markdown 格式的分析报告文件 + +# 不使用 set -e,因为某些检查命令可能失败但不应中断脚本 +set +e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# 格式化输出函数 +print_header() { + echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}" + echo -e "${BLUE}$1${NC}" + echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n" +} + +print_section() { + echo -e "\n${YELLOW}─── $1 ───${NC}\n" +} + +print_service() { + printf " %-50s %10s\n" "$1" "$2" +} + +print_conclusion() { + echo -e "\n${GREEN}═══════════════════════════════════════════════════════════${NC}" + echo -e "${GREEN}分析结论${NC}" + echo -e "${GREEN}═══════════════════════════════════════════════════════════${NC}\n" +} + +# 获取 systemd 用户服务的时间戳(微秒) +get_timestamp() { + local service="$1" + local prop="$2" + systemctl --user show "$service" -p "$prop" --no-pager 2>/dev/null | cut -d= -f2 +} + +# 计算时间差(毫秒) +calc_ms() { + local start="$1" + local end="$2" + if [ "$start" = "0" ] || [ "$end" = "0" ] || [ -z "$start" ] || [ -z "$end" ]; then + echo "N/A" + return + fi + echo $(( (end - start) / 1000 )) +} + +# 计算相对于基准的时间(毫秒) +calc_rel_ms() { + local base="$1" + local ts="$2" + if [ "$base" = "0" ] || [ "$ts" = "0" ] || [ -z "$base" ] || [ -z "$ts" ]; then + echo "N/A" + return + fi + echo $(( (ts - base) / 1000 )) +} + +print_header "DDE 启动性能分析" + +# ============================================================ +# 第一步:收集所有时间戳数据 +# ============================================================ + +# 辅助函数:从 target.wants 目录读取服务列表 +get_target_services() { + local target="$1" + local dir="/usr/lib/systemd/user/${target}.wants" + if [ -d "$dir" ]; then + ls "$dir" 2>/dev/null | sed 's/\.service$//' | sort + fi +} + +# 辅助函数:获取服务的时间戳 +get_svc_timestamps() { + local svc="$1" + local start=$(get_timestamp "$svc" "ExecMainStartTimestampMonotonic") + local ready=$(get_timestamp "$svc" "ActiveEnterTimestampMonotonic") + echo "$start:$ready" +} + +# Target 只有就绪时刻 +PRE_READY=$(get_timestamp "dde-session-pre.target" "ActiveEnterTimestampMonotonic") +CORE_READY=$(get_timestamp "dde-session-core.target" "ActiveEnterTimestampMonotonic") +INIT_READY=$(get_timestamp "dde-session-initialized.target" "ActiveEnterTimestampMonotonic") +SESSION_READY=$(get_timestamp "dde-session.target" "ActiveEnterTimestampMonotonic") + +# 找出最早启动的服务作为基准 T=0(使用启动时刻,从 pre 阶段服务中找) +BASELINE="" +BASELINE_NAME="" + +# Pre 阶段的额外服务(不在 target.wants 中,但需要用于基准检测) +PRE_EXTRA_SERVICES="dde-session-manager" + +# 检查所有 Pre 阶段服务的启动时刻 +for svc in $(get_target_services "dde-session-pre.target") $PRE_EXTRA_SERVICES; do + ts=$(get_svc_timestamps "${svc}.service") + start="${ts%%:*}" + if [ -n "$start" ] && [ "$start" != "0" ]; then + if [ -z "$BASELINE" ] || [ "$start" -lt "$BASELINE" ]; then + BASELINE=$start + BASELINE_NAME=$svc + fi + fi +done + +if [ -z "$BASELINE" ] || [ "$BASELINE" = "0" ]; then + echo -e "${RED}错误:无法获取服务时间戳,可能 DDE 未运行${NC}" + exit 1 +fi + +# 计算关键时间点(相对于基准启动时刻) +PRE_REL=$(calc_rel_ms $BASELINE $PRE_READY) +CORE_REL=$(calc_rel_ms $BASELINE $CORE_READY) +INIT_REL=$(calc_rel_ms $BASELINE $INIT_READY) +SESSION_REL=$(calc_rel_ms $BASELINE $SESSION_READY) + +# 辅助函数:获取服务的时间戳 +get_svc_timestamps() { + local svc="$1" + local start=$(get_timestamp "$svc" "ExecMainStartTimestampMonotonic") + local ready=$(get_timestamp "$svc" "ActiveEnterTimestampMonotonic") + echo "$start:$ready" +} + +# 辅助函数:打印服务信息(显示启动时刻、就绪时刻、耗时) +print_svc() { + local name="$1" + local start_ts="$2" + local ready_ts="$3" + local note="$4" + + # 计算相对时间 + local start_rel=$(calc_rel_ms $BASELINE $start_ts) + local ready_rel=$(calc_rel_ms $BASELINE $ready_ts) + local duration=$(calc_ms $start_ts $ready_ts) + + # 格式化时间字符串 + local start_str ready_str + if [ "$name" = "$BASELINE_NAME" ]; then + start_str="T+0ms" + note="基准" + else + start_str="T+${start_rel}ms" + fi + ready_str="T+${ready_rel}ms" + + printf " %-40s %10s %10s %8s %s\n" "$name" "$start_str" "$ready_str" "$duration" "$note" +} + +# 辅助函数:打印 Target(只有就绪时刻) +print_target() { + local name="$1" + local ready_ts="$2" + local note="$3" + local ready_rel=$(calc_rel_ms $BASELINE $ready_ts) + printf " %-40s %10s %10s %8s %s\n" "$name" "--" "T+${ready_rel}ms" "--" "$note" +} + +# 辅助函数:打印一个阶段的所有服务 +print_phase_services() { + local target="$1" + local ready_ts="$2" + local note="$3" + local extra_svcs="$4" # 额外的服务(不在 target.wants 中) + + local services=$(get_target_services "$target") + + # 收集服务信息到临时文件,便于排序 + local tmpfile=$(mktemp) + local baseline_file=$(mktemp) + + for svc in $services $extra_svcs; do + [ -z "$svc" ] && continue + local ts=$(get_svc_timestamps "${svc}.service") + local start="${ts%%:*}" + local ready="${ts#*:}" + + # 简化显示名称 + local display_name="$svc" + case "$svc" in + "dde-session@x11") display_name="dde-session@x11 (kwin)" ;; + "dde-shell@DDE") display_name="dde-shell@DDE (任务栏)" ;; + "dde-shell-plugin@org.deepin.ds.desktop") display_name="dde-shell-plugin@desktop (桌面)" ;; + esac + + # 检查是否是基准服务 + if [ "$svc" = "$BASELINE_NAME" ]; then + echo "$display_name|$start|$ready" > "$baseline_file" + else + echo "${start:-999999999}|$display_name|$start|$ready" >> "$tmpfile" + fi + done + + # 先打印基准服务(如果在这个阶段) + if [ -s "$baseline_file" ]; then + local line=$(cat "$baseline_file") + local name="${line%%|*}" + local rest="${line#*|}" + local start="${rest%%|*}" + local ready="${rest#*|}" + print_svc "$name" "$start" "$ready" "" + fi + + # 按启动时间排序并打印其他服务 + sort -t'|' -k1 -n "$tmpfile" | while IFS='|' read sortkey name start ready; do + print_svc "$name" "$start" "$ready" "" + done + + # 打印 target + print_target "→ $target" "$ready_ts" "$note" + + rm -f "$tmpfile" "$baseline_file" +} + +# ============================================================ +# 第二步:显示启动时刻表格(最重要的信息) +# ============================================================ + +print_section "1. 启动时刻总览" + +echo "基准服务: $BASELINE_NAME (最早启动的服务)" +echo "" + +printf " %-40s %10s %10s %8s %s\n" "服务" "启动" "就绪" "耗时" "备注" +printf " %-40s %10s %10s %8s %s\n" "────────────────────────────────────" "──────────" "──────────" "────────" "──────────" + +# Pre 阶段(额外包含 dde-session-manager,用于基准检测) +echo " 【dde-session-pre.target 阶段】" +print_phase_services "dde-session-pre.target" "$PRE_READY" "关键路径 1" \ + "dde-session-manager" + +echo "" +echo " 【dde-session-core.target 阶段】" +print_phase_services "dde-session-core.target" "$CORE_READY" "关键路径 2" "" + +echo "" +echo " 【dde-session-initialized.target 阶段】" +print_phase_services "dde-session-initialized.target" "$INIT_READY" "关键路径 3" "" + +echo "" +print_target "→ dde-session.target" "$SESSION_READY" "完成" + +# ============================================================ +# 第三步:其他分析信息 +# ============================================================ + +print_section "2. systemd-analyze blame (Top 10, 仅显示可信时间)" + +echo "" +echo " 仅显示 Type=notify/dbus 的服务(oneshot/simple 时间可能有 ExecStartPre 干扰,已过滤)" +echo "" +printf " %-50s %12s %10s\n" "服务名称" "耗时" "类型" +printf " %-50s %12s %10s\n" "──────────────────────────────────────────────────" "────────────" "──────────" + +# 获取所有服务并只保留 Type=notify/dbus(oneshot 可能有 ExecStartPre 干扰) +tmpfile=$(mktemp) +systemd-analyze --user blame 2>/dev/null | while read time service; do + svc_type=$(systemctl --user show "$service" -p Type --no-pager 2>/dev/null | cut -d= -f2) + # 只保留 notify 和 dbus 类型(这些类型的 blame 时间才是真实的启动耗时) + if [ "$svc_type" = "notify" ] || [ "$svc_type" = "dbus" ]; then + echo "$time|$service|$svc_type" + fi +done > "$tmpfile" + +# 显示前 10 个 +head -10 "$tmpfile" | while IFS='|' read time service svc_type; do + printf " %-50s %12s %10s\n" "$service" "$time" "${svc_type:-unknown}" +done + +rm -f "$tmpfile" + +echo "" +echo " 注:Type=notify/dbus 的服务,时间是进程启动到通知就绪的真实耗时" + +print_section "3. systemd 用户会话进程列表 (Top 50)" + +echo "" +printf " %-8s %-8s %-10s %-7s %-40s %s\n" "PID" "PPID" "启动时间" "内存" "服务名" "命令行" +printf " %-8s %-8s %-10s %-7s %-40s %s\n" "--------" "--------" "----------" "-------" "----------------------------------------" "----------------------------------------" + +# 获取所有 systemd --user 管理的运行中服务 +tmpfile=$(mktemp) + +# 获取服务的 MainPID 和启动时间 +for svc in $(systemctl --user list-units --type=service --state=running --no-pager 2>/dev/null | awk '{print $1}'); do + main_pid=$(systemctl --user show "$svc" -p MainPID --no-pager 2>/dev/null | cut -d= -f2) + start_ts=$(systemctl --user show "$svc" -p ExecMainStartTimestampMonotonic --no-pager 2>/dev/null | cut -d= -f2) + + if [ -n "$main_pid" ] && [ "$main_pid" != "0" ]; then + # 计算相对启动时间 + start_rel=$(calc_rel_ms $BASELINE $start_ts) + echo "${start_rel:-999999999}|${main_pid}|${svc}" >> "$tmpfile" + fi +done + +# 按启动时间排序并显示前 50 个 +sort -t'|' -k1 -n "$tmpfile" | head -50 | while IFS='|' read start_rel pid svc; do + [ -z "$pid" ] || [ "$pid" = "0" ] && continue + + # 获取进程信息 + if [ -d "/proc/$pid" ]; then + ppid=$(awk '/PPid:/ {print $2}' /proc/$pid/status 2>/dev/null) + cmdline=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null | sed 's/ $//') + mem_kb=$(awk '/VmRSS:/ {print $2}' /proc/$pid/status 2>/dev/null) + + # 格式化内存 + if [ -n "$mem_kb" ]; then + if [ "$mem_kb" -gt 1024 ]; then + mem_str="$((mem_kb / 1024))M" + else + mem_str="${mem_kb}K" + fi + else + mem_str="-" + fi + + # 格式化启动时间 + if [ "$start_rel" = "0" ]; then + time_str="T+0ms" + elif [ "$start_rel" != "999999999" ]; then + time_str="T+${start_rel}ms" + else + time_str="-" + fi + + # 提取服务名(去掉 .service 后缀,并截断到 40 字符) + svc_short=$(echo "$svc" | sed 's/\.service$//' | cut -c1-40) + + # 如果 cmdline 为空,尝试读取 comm + if [ -z "$cmdline" ]; then + cmdline=$(cat /proc/$pid/comm 2>/dev/null) + fi + + printf " %-8s %-8s %-10s %-7s %-40s %s\n" "$pid" "${ppid:- -}" "$time_str" "$mem_str" "$svc_short" "$cmdline" + fi +done + +rm -f "$tmpfile" + +echo "" +echo " 注:按启动时间排序,仅显示运行中的服务进程,服务名超过 40 字符会被截断" + +print_section "4. dde-shell 内部耗时分析" + +# 获取最近一次启动的时间范围(使用 systemd 的启动时间) +SESSION_START_TS=$(get_timestamp "dde-session-manager.service" "ActiveEnterTimestampMonotonic") +if [ -z "$SESSION_START_TS" ] || [ "$SESSION_START_TS" = "0" ]; then + echo " 无法获取会话启动时间,跳过日志分析" +else + # 计算启动时间(当前时间 - 单调时间差) + CURRENT_MONOTONIC=$(date +%s%N) + CURRENT_REALTIME=$(date +%s) + # 简化:使用 --boot 0 获取本次启动的日志 + + echo " 分析本次启动的日志..." + echo "" + + # 分析任务栏进程 (dde-shell@DDE) + echo " 【任务栏进程 dde-shell@DDE】" + SHELL_PID=$(systemctl --user show dde-shell@DDE.service -p MainPID --no-pager 2>/dev/null | cut -d= -f2) + if [ -n "$SHELL_PID" ] && [ "$SHELL_PID" != "0" ]; then + echo " PID: $SHELL_PID" + + # 提取关键日志(使用 --boot 0 获取本次启动) + SHELL_FIRST_LOG=$(journalctl --user -u dde-shell@DDE.service --boot 0 -o short-precise 2>/dev/null | grep -v "^--" | grep "dde-shell\[" | head -1) + SHELL_DCONFIG_ERRORS=$(journalctl --user -u dde-shell@DDE.service --boot 0 2>/dev/null | grep -c "acquire failed" || echo "0") + SHELL_NOTIFY_LOGS=$(journalctl --user -u dde-shell@DDE.service --boot 0 2>/dev/null | grep -i "notification" | head -5) + + if [ -n "$SHELL_FIRST_LOG" ]; then + FIRST_LOG_SHORT=$(echo "$SHELL_FIRST_LOG" | cut -c1-100) + echo " 首条日志: ${FIRST_LOG_SHORT}..." + fi + + if [ "$SHELL_DCONFIG_ERRORS" -gt 0 ] 2>/dev/null; then + echo -e " ${YELLOW}⚠ DConfig acquire 失败: ${SHELL_DCONFIG_ERRORS} 次${NC}" + else + echo -e " ${GREEN}✓ 无 DConfig 错误${NC}" + fi + + # 检测 notificationserver 相关日志 + if echo "$SHELL_NOTIFY_LOGS" | grep -q "NotificationManager\|DBAccessor"; then + echo -e " ${YELLOW}⚠ 检测到 NotificationManager/DBAccessor 日志(可能在 init 中执行重操作)${NC}" + fi + else + echo " 服务未运行或无法获取进程信息" + fi + + echo "" + + # 分析桌面进程 (dde-shell-plugin@org.deepin.ds.desktop) + echo " 【桌面进程 dde-shell-plugin@org.deepin.ds.desktop】" + DESKTOP_PID=$(systemctl --user show dde-shell-plugin@org.deepin.ds.desktop.service -p MainPID --no-pager 2>/dev/null | cut -d= -f2) + if [ -n "$DESKTOP_PID" ] && [ "$DESKTOP_PID" != "0" ]; then + echo " PID: $DESKTOP_PID" + + # 提取关键日志 + DESKTOP_FIRST_LOG=$(journalctl --user -u dde-shell-plugin@org.deepin.ds.desktop.service --boot 0 -o short-precise 2>/dev/null | grep -v "^--" | grep "dde-shell-plugin\[" | head -1) + DESKTOP_DCONFIG_ERRORS=$(journalctl --user -u dde-shell-plugin@org.deepin.ds.desktop.service --boot 0 2>/dev/null | grep -c "acquire failed" || echo "0") + DESKTOP_NOREPLY=$(journalctl --user -u dde-shell-plugin@org.deepin.ds.desktop.service --boot 0 2>/dev/null | grep "NoReply" | head -1) + + if [ -n "$DESKTOP_FIRST_LOG" ]; then + FIRST_LOG_SHORT=$(echo "$DESKTOP_FIRST_LOG" | cut -c1-100) + echo " 首条日志: ${FIRST_LOG_SHORT}..." + fi + + if [ "$DESKTOP_DCONFIG_ERRORS" -gt 0 ] 2>/dev/null; then + echo -e " ${YELLOW}⚠ DConfig acquire 失败: ${DESKTOP_DCONFIG_ERRORS} 次${NC}" + else + echo -e " ${GREEN}✓ 无 DConfig 错误${NC}" + fi + + # 检测壁纸 DBus 超时(最关键的瓶颈) + if [ -n "$DESKTOP_NOREPLY" ]; then + echo -e " ${RED}✗ 检测到 DBus NoReply 超时(壁纸服务未就绪)${NC}" + NOREPLY_SHORT=$(echo "$DESKTOP_NOREPLY" | cut -c1-100) + echo " 详情: ${NOREPLY_SHORT}..." + echo -e " ${YELLOW}建议: 优先从本地配置读取壁纸,DBus 调用改为异步${NC}" + else + echo -e " ${GREEN}✓ 未检测到 DBus 超时${NC}" + fi + else + echo " 服务未运行或无法获取进程信息" + fi +fi + +print_section "5. 已安装的 systemd 服务状态" + +# 检查实际安装的服务文件 +echo " 检查系统已安装的服务配置文件..." +echo "" + +# 检查 XSettings1 +XSETTINGS_FILE="/usr/lib/systemd/user/org.deepin.dde.XSettings1.service" +if [ -f "$XSETTINGS_FILE" ]; then + if grep -q "Before=org.dde.session.Daemon1" "$XSETTINGS_FILE" 2>/dev/null; then + echo -e " ${RED}✗${NC} $XSETTINGS_FILE" + echo " 仍存在 Before=org.dde.session.Daemon1.service(串行依赖)" + else + echo -e " ${GREEN}✓${NC} $XSETTINGS_FILE" + echo " 已移除串行依赖" + fi +fi + +# 检查 Daemon1 +DAEMON_FILE="/usr/lib/systemd/user/org.dde.session.Daemon1.service" +if [ -f "$DAEMON_FILE" ]; then + if grep -q "Before=dde-session@x11" "$DAEMON_FILE" 2>/dev/null; then + echo -e " ${RED}✗${NC} $DAEMON_FILE" + echo " 仍存在 Before=dde-session@x11.service(阻塞 kwin)" + else + echo -e " ${GREEN}✓${NC} $DAEMON_FILE" + echo " 已移除串行依赖" + fi +fi + +# 检查 dde-shell@DDE +SHELL_FILE="/usr/lib/systemd/user/dde-shell@DDE.service" +if [ -f "$SHELL_FILE" ]; then + SHELL_TYPE_VAL=$(grep -E "^Type=" "$SHELL_FILE" 2>/dev/null | cut -d= -f2) + if [ "$SHELL_TYPE_VAL" = "notify" ]; then + echo -e " ${GREEN}✓${NC} $SHELL_FILE" + echo " Type=notify 已配置" + else + echo -e " ${YELLOW}!${NC} $SHELL_FILE" + echo " Type=$SHELL_TYPE_VAL,建议改为 notify" + fi +fi + +# 检查 kwin ExecStartPre +KWIN_FILE="/usr/lib/systemd/user/dde-session@x11.service" +if [ -f "$KWIN_FILE" ]; then + if grep -q "ExecStartPre=" "$KWIN_FILE" 2>/dev/null; then + echo -e " ${YELLOW}!${NC} $KWIN_FILE" + echo " 存在 ExecStartPre 命令,建议移除或移出关键路径" + else + echo -e " ${GREEN}✓${NC} $KWIN_FILE" + echo " 无 ExecStartPre 阻塞" + fi +fi + +print_section "6. 优化建议汇总" + +SUGGESTIONS=() + +# 检查 kwin ExecStartPre +KWIN_EXEC_PRE=$(systemctl --user show dde-session@x11.service -p ExecStartPre --no-pager 2>/dev/null) +if [[ "$KWIN_EXEC_PRE" == *"ExecStartPre"* ]] && [[ "$KWIN_EXEC_PRE" != *"ExecStartPre=" ]]; then + SUGGESTIONS+=("${YELLOW}[P2]${NC} kwin 存在 ExecStartPre 命令,建议合并或移除(可节省 10-20ms)") +fi + +# 检查 file-manager 是否阻塞 initialized +FM_BEFORE=$(systemctl --user show dde-file-manager.service -p Before --no-pager 2>/dev/null | cut -d= -f2) +if [[ "$FM_BEFORE" == *"initialized"* ]]; then + FM_START_TS=$(get_timestamp "dde-file-manager.service" "ExecMainStartTimestampMonotonic") + FM_READY_TS=$(get_timestamp "dde-file-manager.service" "ActiveEnterTimestampMonotonic") + FM_DURATION=$(calc_ms $FM_START_TS $FM_READY_TS) + SUGGESTIONS+=("${YELLOW}[P1]${NC} dde-file-manager 阻塞 initialized.target (启动耗时 ${FM_DURATION}ms),建议移出关键路径") +fi + +# 检查 dde-shell 类型 +SHELL_TYPE=$(systemctl --user show dde-shell@DDE.service -p Type --no-pager 2>/dev/null | cut -d= -f2) +if [ "$SHELL_TYPE" = "simple" ]; then + SUGGESTIONS+=("${YELLOW}[P1]${NC} dde-shell@DDE 为 Type=simple,建议改为 Type=notify + sd_notify(READY=1)") +fi + +# 检查 AM 是否在 pre 阶段 +AM_AFTER=$(systemctl --user show org.desktopspec.ApplicationManager1.service -p After --no-pager 2>/dev/null | cut -d= -f2) +if [[ "$AM_AFTER" == *"dde-session-pre.target"* ]]; then + SUGGESTIONS+=("${GREEN}[P0 已完成]${NC} AM 提前到 graphical-session-pre 阶段(与 kwin 并行)") +else + SUGGESTIONS+=("${YELLOW}[P0]${NC} AM 应提前到 graphical-session-pre.target,与 kwin 并行启动") +fi + +# 从日志分析中提取的建议 +if [ -n "$DESKTOP_NOREPLY" ]; then + SUGGESTIONS+=("${RED}[P0 关键]${NC} 桌面进程存在 DBus NoReply 超时,建议优先从本地读取壁纸") +fi + +if [ "$SHELL_DCONFIG_ERRORS" -gt 5 ] 2>/dev/null || [ "$DESKTOP_DCONFIG_ERRORS" -gt 5 ] 2>/dev/null; then + SUGGESTIONS+=("${YELLOW}[P1]${NC} DConfig acquire 失败次数过多,建议修复资源安装或合并实例") +fi + +# 输出建议 +if [ ${#SUGGESTIONS[@]} -eq 0 ]; then + echo -e " ${GREEN}✓ 未发现明显的优化点,启动流程已优化良好${NC}" +else + echo " 发现 ${#SUGGESTIONS[@]} 个优化建议:" + echo "" + for i in "${!SUGGESTIONS[@]}"; do + echo -e " $((i+1)). ${SUGGESTIONS[$i]}" + done +fi + +print_conclusion + +# 计算总耗时 +TOTAL_TO_DESKTOP=$INIT_REL +TOTAL_TO_SESSION=$SESSION_REL + +echo " 关键路径时间:" +echo " ┌────────────────────────────────────────────────────┐" +printf " │ pre.target: T+%10sms │\n" "$PRE_REL" +printf " │ core.target: T+%10sms │\n" "$CORE_REL" +printf " │ initialized.target: T+%10sms ← 用户看到桌面 │\n" "$INIT_REL" +printf " │ session.target: T+%10sms │\n" "$SESSION_REL" +echo " └────────────────────────────────────────────────────┘" + +echo "" +echo " 参考指标:" +echo " • 正常范围:initialized.target < 1200ms" +echo " • 优化目标:initialized.target < 1000ms" +echo " • 优秀表现:initialized.target < 800ms" +echo "" + +if [ "$INIT_REL" != "N/A" ] && [ "$INIT_REL" -gt 1200 ]; then + echo -e " ${RED}⚠ 启动时间偏慢 (${INIT_REL}ms),建议实施优化${NC}" +elif [ "$INIT_REL" != "N/A" ] && [ "$INIT_REL" -gt 1000 ]; then + echo -e " ${YELLOW}! 启动时间中等 (${INIT_REL}ms),还有优化空间${NC}" +elif [ "$INIT_REL" != "N/A" ]; then + echo -e " ${GREEN}✓ 启动时间优秀 (${INIT_REL}ms)${NC}" +fi + +echo "" +echo "═══════════════════════════════════════════════════════════" +echo "分析报告生成完毕" +echo "═══════════════════════════════════════════════════════════" + +# 生成 Markdown 报告 +generate_markdown_report() { + local report_file="boot-analysis-$(date +%Y%m%d-%H%M%S).md" + cat > "$report_file" << MDHEADER +# DDE 启动性能分析报告 + +> 生成时间:$(date '+%Y-%m-%d %H:%M:%S') + +MDHEADER + echo "" >> "$report_file" + + echo "## 1. systemd-analyze blame (Top 10)" >> "$report_file" + echo '```' >> "$report_file" + systemd-analyze --user blame 2>/dev/null | head -10 >> "$report_file" + echo '```' >> "$report_file" + echo "" >> "$report_file" + + echo "## 2. 关键时间线" >> "$report_file" + echo "" >> "$report_file" + echo "| Target | 时刻 (T+) |" >> "$report_file" + echo "|--------|----------|" >> "$report_file" + echo "| pre.target | ${PRE_REL}ms |" >> "$report_file" + echo "| core.target | ${CORE_REL}ms |" >> "$report_file" + echo "| initialized.target | ${INIT_REL}ms |" >> "$report_file" + echo "| session.target | ${SESSION_REL}ms |" >> "$report_file" + echo "" >> "$report_file" + + echo "## 3. 依赖关系检查" >> "$report_file" + echo "" >> "$report_file" + if [[ "$XSETTINGS_BEFORE" == *"Daemon1"* ]] || [[ "$DAEMON_BEFORE" == *"dde-session@x11"* ]]; then + echo "- [ ] XSettings1 → Daemon1 → kwin 串行依赖 **待优化**" >> "$report_file" + else + echo "- [x] XSettings1/Daemon1/kwin 并行启动" >> "$report_file" + fi + echo "" >> "$report_file" + + echo "## 4. 优化建议" >> "$report_file" + echo "" >> "$report_file" + echo "1. 移除 XSettings1.Before=Daemon1" >> "$report_file" + echo "2. 移除 Daemon1.Before=dde-session@x11" >> "$report_file" + echo "3. dde-shell@DDE 改为 Type=notify" >> "$report_file" + echo "4. kwin ExecStartPre 移出关键路径" >> "$report_file" + echo "" >> "$report_file" + + echo "报告已保存到:$report_file" +} + +# 如果传入 --report 参数,生成 Markdown 报告 +if [ "$1" = "--report" ]; then + generate_markdown_report + exit 0 +fi diff --git a/tools/analyze-memory.sh b/tools/analyze-memory.sh new file mode 100755 index 0000000..aee4a45 --- /dev/null +++ b/tools/analyze-memory.sh @@ -0,0 +1,347 @@ +#!/bin/bash +# DDE 内存分析脚本 +# 用法:sudo ./analyze-memory.sh [--report] +# 输出:DDE各进程内存占用情况,便于针对性优化 +# 注意:需要 root 权限以读取所有进程的 PSS 信息 + +set +e + +# 检查是否以 root 用户运行 +if [ "$(id -u)" -ne 0 ]; then + echo "错误:此脚本需要 root 权限运行" + echo "用法:sudo $0 [--report]" + exit 1 +fi + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +print_header() { + echo -e "\n${BLUE}═══════════════════════════════════════════════════════════${NC}" + echo -e "${BLUE}$1${NC}" + echo -e "${BLUE}═══════════════════════════════════════════════════════════${NC}\n" +} + +print_section() { + echo -e "\n${YELLOW}─── $1 ───${NC}\n" +} + +# 格式化内存大小 +format_memory() { + local kb="$1" + [ -z "$kb" ] || [ "$kb" = "0" ] && { echo "-"; return; } + [ "$kb" -ge 1048576 ] && { echo "$(echo "scale=2; $kb/1048576" | bc)G"; return; } + [ "$kb" -ge 1024 ] && { echo "$((kb / 1024))M"; return; } + echo "${kb}K" +} + +# 获取进程 RSS (KB) +get_pid_rss() { + [ -f "/proc/$1/status" ] && awk '/VmRSS:/ {print $2}' /proc/$1/status 2>/dev/null || echo 0 +} + +# 获取进程共享内存 (KB) +get_pid_shared() { + [ -f "/proc/$1/status" ] && awk '/RssFile:/ {print $2}' /proc/$1/status 2>/dev/null || echo 0 +} + +# 获取进程 PSS (KB) - 优先使用 smaps_rollup,性能更好 +get_pid_pss() { + local pss=0 + # 优先使用 smaps_rollup (Linux 4.14+),内核直接计算,性能极佳 + if [ -f "/proc/$1/smaps_rollup" ]; then + pss=$(awk '/^Pss:/ {print $2}' /proc/$1/smaps_rollup 2>/dev/null) + fi + # 降级:使用 smaps 遍历累加 + if [ -z "$pss" ] || [ "$pss" = "0" ]; then + if [ -f "/proc/$1/smaps" ]; then + pss=$(awk '/^Pss:/ {sum += $2} END {print sum}' /proc/$1/smaps 2>/dev/null) + fi + fi + echo "${pss:-0}" +} + +print_header "DDE 内存占用分析" + +echo "分析时间: $(date '+%Y-%m-%d %H:%M:%S')" +echo "系统版本: $(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')" + +# 获取系统总内存(提前计算,后续部分会用到) +MEM_TOTAL=$(awk '/MemTotal:/ {print $2}' /proc/meminfo) +MEM_FREE=$(awk '/MemFree:/ {print $2}' /proc/meminfo) +MEM_AVAILABLE=$(awk '/MemAvailable:/ {print $2}' /proc/meminfo) +MEM_USED=$((MEM_TOTAL - MEM_AVAILABLE)) + +# 获取 DDE 进程内存信息 +dde_tmpfile=$(mktemp) + +# 使用 awk 精确匹配命令列(第11列起),避免用户名列干扰,同时排除脚本自身 +ps aux | awk -v self_pid="$$" 'NR>1 && $2!=self_pid {cmd=""; for(i=11;i<=NF;i++) cmd=cmd $i " "; if(cmd ~ /(dde|deepin|service-manager|tray|kwin)/ && cmd !~ /(uos-license-agent|deepin-anything|deepin-elf-verify)/) print $2}' | while read pid; do + if [ -d "/proc/$pid" ]; then + rss=$(get_pid_rss "$pid") + shared=$(get_pid_shared "$pid") + pss=$(get_pid_pss "$pid") + total=$((rss + shared)) + comm=$(cat /proc/$pid/comm 2>/dev/null || echo "unknown") + echo "${total}|${rss}|${shared}|${pss}|${pid}|${comm}" + fi +done | sort -t'|' -k1 -rn > "$dde_tmpfile" + +DDE_TOTAL_PSS=0 +DDE_TOTAL_RSS=0 +DDE_PROCESS_COUNT=0 + +while IFS='|' read total rss shared pss pid comm; do + DDE_TOTAL_PSS=$((DDE_TOTAL_PSS + pss)) + DDE_TOTAL_RSS=$((DDE_TOTAL_RSS + rss)) + DDE_PROCESS_COUNT=$((DDE_PROCESS_COUNT + 1)) +done < "$dde_tmpfile" + +DDE_PERCENT=$(echo "scale=2; $DDE_TOTAL_PSS * 100 / $MEM_TOTAL" | bc) +DDE_PSS_PERCENT=$(echo "scale=2; $DDE_TOTAL_PSS * 100 / $MEM_USED" | bc) + +# ============================================================ +# 第一部分:系统内存概览 +# ============================================================ + +print_section "1. 系统内存概览" + +MEM_TOTAL_STR=$(format_memory $MEM_TOTAL) +MEM_USED_STR=$(format_memory $MEM_USED) +MEM_AVAILABLE_STR=$(format_memory $MEM_AVAILABLE) +MEM_USED_PCT=$(echo "scale=1; $MEM_USED * 100 / $MEM_TOTAL" | bc) +MEM_AVAILABLE_PCT=$(echo "scale=1; $MEM_AVAILABLE * 100 / $MEM_TOTAL" | bc) + +echo "" +echo " 项目 大小 占比" +echo " ---------------- ---------- --------" +echo " 物理内存总量 $MEM_TOTAL_STR 100%" +echo " 已使用 $MEM_USED_STR ${MEM_USED_PCT}%" +echo " 可用内存 $MEM_AVAILABLE_STR ${MEM_AVAILABLE_PCT}%" + +# ============================================================ +# 第二部分:系统进程内存占用列表(按内存排序) +# ============================================================ + +print_section "2. 系统进程内存占用列表 (Top 100, 按内存排序)" + +echo "" +echo " PID 用户 RSS 共享 PSS 进程名 命令行" +echo " -------- ------------ -------- -------- -------- -------------------- --------------------------------------------------------------------------" + +# 获取系统所有进程,按内存排序 +ps aux --sort=-rss | head -101 | tail -100 | while read -r user pid cpu mem vsz rss_kb tty stat start time comm; do + [ -z "$pid" ] && continue + + if [ -d "/proc/$pid" ]; then + shared_kb=$(get_pid_shared "$pid") + pss_kb=$(get_pid_pss "$pid") + + # 格式化内存 + rss_str=$(format_memory "$rss_kb") + shared_str=$(format_memory "$shared_kb") + pss_str=$(format_memory "$pss_kb") + + # 获取进程名 + proc_name=$(cat /proc/$pid/comm 2>/dev/null || echo "$comm" | awk '{print $1}') + proc_name=$(echo "$proc_name" | cut -c1-20) + + # 获取完整命令行 + cmdline=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null | sed 's/ $//') + if [ -z "$cmdline" ]; then + # 如果 cmdline 为空,尝试读取 exe 链接 + cmdline=$(readlink /proc/$pid/exe 2>/dev/null) + fi + [ -z "$cmdline" ] && cmdline=$(cat /proc/$pid/comm 2>/dev/null) + + # 截取命令行到90个字符 + cmdline=$(echo "$cmdline" | cut -c1-90) + + # 截取用户名 + user_short=$(echo "$user" | cut -c1-12) + + printf " %-8s %-12s %-8s %-8s %-8s %-20s %s\n" "$pid" "$user_short" "$rss_str" "$shared_str" "$pss_str" "$proc_name" "$cmdline" + fi +done + +echo "" +echo " 注:按内存占用排序,显示系统所有进程的前 100 个" + +# ============================================================ +# 第三部分:DDE 进程内存占用 +# ============================================================ + +print_section "3. DDE 进程内存占用" + +echo "" +echo " PID RSS 共享 PSS 进程名 命令行" +echo " -------- -------- -------- -------- -------------------- --------------------------------------------------------------------------" + +# 显示所有 DDE 进程 +while IFS='|' read total rss shared pss pid comm; do + cmdline=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null | cut -c1-90) + [ -z "$cmdline" ] && cmdline=$comm + comm=$(echo "$comm" | cut -c1-20) + printf " %-8s %-8s %-8s %-8s %-20s %s\n" "$pid" "$(format_memory $rss)" "$(format_memory $shared)" "$(format_memory $pss)" "$comm" "$cmdline" +done < "$dde_tmpfile" + + # DDE 内存汇总 +echo "" +echo " 项目 数值" +echo " ---------------- ----------------" +echo " DDE 进程数 $DDE_PROCESS_COUNT" +echo " DDE 内存(PSS) $(format_memory $DDE_TOTAL_PSS) (占系统 ${DDE_PERCENT}%)" +echo " 占已用内存 ${DDE_PSS_PERCENT}%" +echo " ────────────────────────────────────" +echo " DDE 内存(RSS) $(format_memory $DDE_TOTAL_RSS)" +echo "" +echo " 【指标说明】" +echo " RSS - 进程物理内存总量,已包含共享内存(私有+共享库+共享内存段)" +echo " 共享 - RSS 中来自文件映射的部分(如 .so 共享库),是 RSS 的子集" +echo " PSS - 按比例分配共享内存后的实际物理占用,多进程累加不会重复计算" +echo "" +echo " 【示例】某共享库 10M 被 3 个进程使用:" +echo " 每个进程 RSS 都包含完整 10M,累加得 30M(重复计算)" +echo " 每个进程 PSS 只算 10M÷3≈3.3M,累加得 10M(真实占用)" + +# ============================================================ +# 第四部分:总结 +# ============================================================ + +print_section "4. 分析总结" + +echo "" +echo " 【内存占用 Top 5 DDE 进程】" +echo "" + +# 获取 Top 5 进程信息 +top5_file=$(mktemp) +head -5 "$dde_tmpfile" | while IFS='|' read total rss shared pss pid comm; do + cmdline=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null | cut -c1-50) + echo "${total}|${rss}|${shared}|${pss}|${pid}|${comm}|${cmdline}" +done > "$top5_file" + +i=1 +while IFS='|' read total rss shared pss pid comm cmdline; do + pss_str=$(format_memory $pss) + [ "$pss_str" = "-" ] && pss_str="N/A" + printf " %d. %-25s RSS: %s, 共享: %s, PSS: %s\n" "$i" "$comm" "$(format_memory $rss)" "$(format_memory $shared)" "$pss_str" + i=$((i + 1)) +done < "$top5_file" + +rm -f "$top5_file" +rm -f "$dde_tmpfile" + +echo "" +echo " 【优化建议】" +echo "" + +suggestions=() + +# 检查任务栏 +SHELL_PID=$(pgrep -f "dde-shell.*-C DDE" | head -1) +if [ -n "$SHELL_PID" ]; then + SHELL_RSS=$(get_pid_rss "$SHELL_PID") + if [ "$SHELL_RSS" -gt 204800 ]; then + suggestions+=("${RED}[P0]${NC} 任务栏进程内存超过 200M ($(format_memory $SHELL_RSS)),建议检查插件内存泄漏或延迟加载非必要组件") + elif [ "$SHELL_RSS" -gt 102400 ]; then + suggestions+=("${YELLOW}[P1]${NC} 任务栏进程内存超过 100M ($(format_memory $SHELL_RSS)),建议检查插件内存占用") + fi +fi + +# 检查 dde-daemon +DAEMON_PID=$(pgrep -f "dde-session-daemon" | head -1) +if [ -n "$DAEMON_PID" ]; then + DAEMON_RSS=$(get_pid_rss "$DAEMON_PID") + if [ "$DAEMON_RSS" -gt 102400 ]; then + suggestions+=("${YELLOW}[P1]${NC} dde-daemon 内存超过 100M ($(format_memory $DAEMON_RSS)),建议检查是否存在内存泄漏") + fi +fi + +# 检查总内存占比 +if [ "$(echo "$DDE_PERCENT > 10" | bc)" -eq 1 ]; then + suggestions+=("${YELLOW}[P1]${NC} DDE 总内存占比超过 10% (${DDE_PERCENT}%),建议优化内存使用") +elif [ "$(echo "$DDE_PERCENT > 5" | bc)" -eq 1 ]; then + suggestions+=("${YELLOW}[P2]${NC} DDE 总内存占比 ${DDE_PERCENT}%,处于中等水平") +fi + +# 检查进程数 +if [ "$DDE_PROCESS_COUNT" -gt 40 ]; then + suggestions+=("${YELLOW}[P2]${NC} DDE 相关进程数较多($DDE_PROCESS_COUNT),建议合并部分功能或延迟启动非必要服务") +fi + +# 检查系统内存压力 +MEM_USED_PERCENT=$((MEM_USED * 100 / MEM_TOTAL)) +if [ "$MEM_USED_PERCENT" -gt 80 ]; then + suggestions+=("${RED}[P0]${NC} 系统内存使用率超过 80% (${MEM_USED_PERCENT}%),存在内存压力") +elif [ "$MEM_USED_PERCENT" -gt 60 ]; then + suggestions+=("${YELLOW}[P1]${NC} 系统内存使用率 ${MEM_USED_PERCENT}%,建议关注内存使用") +fi + +if [ ${#suggestions[@]} -eq 0 ]; then + echo -e " ${GREEN}✓ DDE 内存占用在合理范围内${NC}" +else + for suggestion in "${suggestions[@]}"; do + echo -e " - $suggestion" + done +fi + +echo "" +echo "═══════════════════════════════════════════════════════════" +echo "内存分析报告生成完毕" +echo "═══════════════════════════════════════════════════════════" + +# ============================================================ +# 生成 Markdown 报告 +# ============================================================ + +if [ "$1" = "--report" ]; then + REPORT_FILE="memory-analysis-$(date +%Y%m%d-%H%M%S).md" + cat > "$REPORT_FILE" << EOF +# DDE 内存占用分析报告 + +> 生成时间:$(date '+%Y-%m-%d %H:%M:%S') +> 系统版本:$(cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d '"') + +## 1. 系统内存概览 + +| 项目 | 大小 | 占比 | +|------|------|------| +| 物理内存总量 | $(format_memory $MEM_TOTAL) | 100% | +| 已使用 | $(format_memory $MEM_USED) | $(echo "scale=1; $MEM_USED * 100 / $MEM_TOTAL" | bc)% | +| 可用内存 | $(format_memory $MEM_AVAILABLE) | $(echo "scale=1; $MEM_AVAILABLE * 100 / $MEM_TOTAL" | bc)% | + +## 2. DDE 内存占用统计 + +| 项目 | 数值 | +|------|------| +| DDE 进程数 | $DDE_PROCESS_COUNT | +| DDE 内存(PSS) | $(format_memory $DDE_TOTAL_PSS) (占系统 ${DDE_PERCENT}%) | +| 占已用内存 | ${DDE_PSS_PERCENT}% | +| DDE 内存(RSS) | $(format_memory $DDE_TOTAL_RSS) | + +> **指标说明** +> - **RSS**:进程物理内存总量,已包含共享内存(私有+共享库+共享内存段) +> - **共享**:RSS 中来自文件映射的部分(如 .so 共享库),是 RSS 的子集 +> - **PSS**:按比例分配共享内存后的实际物理占用,多进程累加不会重复计算 +> +> **示例**:某共享库 10M 被 3 个进程使用时,每个进程 RSS 都包含完整 10M(累加得 30M),而 PSS 只算 3.3M(累加得 10M)。 + +## 3. 优化建议 + +EOF + + for suggestion in "${suggestions[@]}"; do + # 先用 printf '%b' 渲染转义序列,再清理颜色代码 + clean=$(printf '%b' "$suggestion" | sed 's/\x1b\[[0-9;]*m//g') + echo "- $clean" >> "$REPORT_FILE" + done + + echo "" >> "$REPORT_FILE" + echo "报告已保存到:$REPORT_FILE" +fi