Android性能分析工具Systrace

戈壁老王

Systrace是Android系统中性能分析的主要工具。它记录一段时间内的设备活动,并产生一个HTML格式的Systrace报告。它收集了CPU调度、磁盘操作、应用线程等信息,可以帮助开发者分析系统瓶颈,改进性能。Systrace实际上是一些工具的集合,在设备端使用Atrace来控制用户空间的tracing和设置ftrace,kernel中使用tracing机制进行数据采集。Systrace最终读取ftrace的buffer,然后将数据打包以HTML文件的方式呈现。

Systrace的使用

在Android Studio 3.0之前的版本中可以通过菜单打开DDMS来使用Systrace,但之后的版本中DDMS被移除了,尽管可以通过命令方式启动DDMS,但它正逐渐被抛弃,取而代之的是Android Profiler。其中的CPU Profiler就提供了与Systrace类似的功能,他们使用的都是Kernel tracing的数据。所以我们不再介绍图形化的Systrace,仅谈论命令行模式。

使用Systrace命令前先确保已经安装Android SDK和Python,Systrace命令位于android-sdk/platform-tools/systrace/中。通过Python脚本来运行Systrace,

$ python systrace.py [options] [categories]

其中options支持的选项如下,

options 描述
-o < FILE > 输出的目标文件
-t N, –time=N 执行时间,默认5s
-b N, –buf-size=N buffer大小(单位kB),用于限制trace总大小,默认无上限
-k < KFUNCS >,–ktrace=< KFUNCS > 追踪kernel函数,用逗号分隔
-a < APP_NAME >,–app=< APP_NAME > 追踪应用包名,用逗号分隔
–from-file=< FROM_FILE > 从文件中创建互动的systrace
-e < DEVICE_SERIAL >,–serial=< DEVICE_SERIAL > 指定设备
-l, –list-categories 列举可用的tags

categories为需要Tracing的模块,当前系统支持的categories可通过命令获取,

$ python systrace.py --list-categories

常见的categories包括,

         gfx - Graphics
       input - Input
        view - View System
     webview - WebView
          wm - Window Manager
          am - Activity Manager
          sm - Sync Manager
       audio - Audio
       video - Video
      camera - Camera
         hal - Hardware Modules
         app - Application
         res - Resource Loading
      dalvik - Dalvik VM
          rs - RenderScript
      bionic - Bionic C Library
       power - Power Management
       sched - CPU Scheduling
        freq - CPU Frequency
        idle - CPU Idle
        load - CPU Load

Systrace执行的结果会生成一个HTML的报告,例如下面的执行结果会保存在mynewtrace.html中。

$ python systrace.py -o mynewtrace.html sched freq idle am wm gfx view input

使用Chrome打开mynewtrace.html,你tracing的模块将以图形化的方式展示出来。一个图形化的Systrace报告大致的展示如下,

systrace_1.png

通过快捷键可以调整视图,快捷键列表如下,

Key Description
W 缩小时间轴
A 沿时间轴左移
S 放大时间轴
D 沿时间线右轴
E 将当前鼠标位置在时间轴上居中
M 标记当前的选择
1 选择模式,双击将高亮相同任务
2 平移模式,拖动平移视图
3 缩放模式,拖动鼠标实现放大/缩小
4 时移模式,拖动来创建或移除时间窗口线
G 在当前选定任务的开始处显示网格。
Shift + G 在当前选定任务的结束处显示网格
Left Arrow 在当前时间轴上选择上一个事件。
Right Arrow 在当前时间轴上选择下一个事件。

通过查看Systrace视图报告,可以查找并修复性能问题。下面的步骤可以帮助快速确定性能问题点,

  • 使用矩形选择感兴趣的时间间隔。
  • 使用标尺标记或高亮发生问题的区域。
  • 通过”View Options > Highlight VSync“来展示每一个显示刷新操作。

如下图所示,Systrace报告列出了UI显示相关的每个进程,并沿时间轴标记了每一个显示帧。用绿色圆圈表示在16.6毫秒(每秒60帧)内呈现的,能稳定显示的帧。渲染时间超过16.6毫秒的帧用黄色或红色的圆圈表示。

systrace_2.png

点击帧标识的圆圈使其高亮,它将提供有关系统为渲染该帧所做的工作的其他信息,包括警报。报告还显示了在渲染该帧时系统正在执行的方法,分析这些方法可以确定UI卡顿的潜在原因。

systrace_3.png

点击异常帧后,可以在报告的底部窗口中看到警报。上图所示的警报表明,该帧的主要问题是ListView回收和重新绑定花费了太多时间。继续跟踪相关事件的链接,可以详细看到系统在这段时间内的工作。

要查看报告中发现的所有警报以及设备触发警报的次数,单击窗口最右侧的Alerts选项卡,如下图所示。Alerts面板可帮助查看跟踪中出现哪些问题,以及它们导致卡顿的频率如何。您可以将此面板视为要修复的问题列表。通常,一个小的修改或优化可以消灭整个一类警报。

systrace_4.png

如果您看到UI线程上运行了太多工作,可以使用以下方法之一来帮助确定哪些方法消耗了太多的CPU时间:

  • 如果您对可能导致瓶颈的方法有所了解,可以在这些方法中添加跟踪标记。参考下诉的”定义自己的tracing“。
  • 如果不确定UI瓶颈的根源,可以使用Android Studio中提供的CPU Profiler继续跟踪。通过Debug类生成跟踪日志,然后使用CPU Profiler导入和检查它们。

Atrace用法

Atrace可以认为是Systrace工具的一部分,其运行在设备端,用于控制ftrace。一般情况下,可以不需要关心Atrace,因为Atrace是由Systrace通过ADB进行控制的。我们简单介绍一下Atrace的使用,也许某一天真的用得上。Atrace的使用帮助如下,

usage: atrace [options] [categories...]
options include:
  -a appname      enable app-level tracing for a comma separated list of cmdlines; * is a wildcard matching any process
  -b N            use a trace buffer size of N KB
  -c              trace into a circular buffer
  -f filename     use the categories written in a file as space-separated
                    values in a line
  -k fname,...    trace the listed kernel functions
  -n              ignore signals
  -s N            sleep for N seconds before tracing [default 0]
  -t N            trace for N seconds [default 5]
  -z              compress the trace dump
  --async_start   start circular trace and return immediately
  --async_dump    dump the current contents of circular trace buffer
  --async_stop    stop tracing and dump the current contents of circular
                    trace buffer
  --stream        stream trace to stdout as it enters the trace buffer
                    Note: this can take significant CPU time, and is best
                    used for measuring things that are not affected by
                    CPU performance, like pagecache usage.
  --list_categories
                  list the available tracing categories
 -o filename      write the trace to the specified file instead
                    of stdout.

其中许多选项都和Systrace相同,categories也是一样的。在设备端使用下面命令将tracing抓取到文件中,

# atrace -o /data/mynewtrace -t 10 sched freq idle am wm gfx view input

之后可以在PC中使用Systrace转换为HTML报告,和直接使用Systrace抓取的结果相同

$ python systrace.py --from-file mynewtrace

定义自己的tracing

Systrace默认仅仅跟踪系统级别进程的信息,如果想跟踪自己应用相对系统事件的执行状态,需要在代码中增加自己的tracing。在Java代码中使用Trace类来实现,由beginSection()和endSection()来标记跟踪的始末。实例代码如下,

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Trace.beginSection("MyAdapter.onCreateViewHolder");
        MyViewHolder myViewHolder;
        try {
            myViewHolder = MyViewHolder.newInstance(parent);
        } finally {
            // In try and catch statements, always call "endSection()" in a
            // "finally" block. That way, the method is invoked even when an
            // exception occurs.
            Trace.endSection();
        }
        return myViewHolder;
    }

   @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        Trace.beginSection("MyAdapter.onBindViewHolder");
        try {
            try {
                Trace.beginSection("MyAdapter.queryDatabase");
                RowItem rowItem = queryDatabase(position);
                dataset.add(rowItem);
            } finally {
                Trace.endSection();
            }
            holder.bind(dataset.get(position));
        } finally {
            Trace.endSection();
        }
    }
}

Native中增加跟踪代码包括几个部分,首先定义ATrace函数的指针。

#include <android/trace.h>
#include <dlfcn.h>

void *(*ATrace_beginSection) (const char* sectionName);
void *(*ATrace_endSection) (void);

typedef void *(*fp_ATrace_beginSection) (const char* sectionName);
typedef void *(*fp_ATrace_endSection) (void);

然后在运行时加载ATrace符号,通常在构造函数里完成。

// Retrieve a handle to libandroid.
void *lib = dlopen("libandroid.so", RTLD_NOW || RTLD_LOCAL);

// Access the native tracing functions.
if (lib != NULL) {
    // Use dlsym() to prevent crashes on devices running Android 5.1
    // (API level 22) or lower.
    ATrace_beginSection = reinterpret_cast<fp_ATrace_beginSection>(
        dlsym(lib, "ATrace_beginSection"));
    ATrace_endSEction = reinterpret_cast<fp_ATrace_endSection>(
        dlsym(lib, "ATrace_endSection"));
}

之后就可以在跟踪的代码前后调用ATrace_beginSection()和ATrace_endSection()。

#include <android/trace.h>

char *customEventName = new char[32];
sprintf(customEventName, "User tapped %s button", buttonName);

ATrace_beginSection(customEventName);
// Your app or game's response to the button being pressed.
ATrace_endSection();

按上述方法修改完代码后,编译生成Debug版本的App。在执行Systrace时,像下例中使用-a来指定App,跟踪的事件将展示在Systrace的报告中。

$ python systrace.py -a com.example.myapp -b 16384 \
  -o my_systrace_report.html sched freq idle am wm gfx view binder_driver hal \
  dalvik camera input res

控制Ftrace

Atrace使用起来很简单,和Systrace几乎一样。Atrace通过配置Ftrace来启动tracing,了解Atrace就能简单了解Ftrace的使用。tracing的内容是通过categories指定的,看一下categories在代码中的处理。

/* Tracing categories */
static const TracingCategory k_categories[] = {
    { "gfx",        "Graphics",         ATRACE_TAG_GRAPHICS, { } },
    { "input",      "Input",            ATRACE_TAG_INPUT, { } },
    { "view",       "View System",      ATRACE_TAG_VIEW, { } },
    { "webview",    "WebView",          ATRACE_TAG_WEBVIEW, { } },
    { "wm",         "Window Manager",   ATRACE_TAG_WINDOW_MANAGER, { } },
    { "am",         "Activity Manager", ATRACE_TAG_ACTIVITY_MANAGER, { } },
    { "sm",         "Sync Manager",     ATRACE_TAG_SYNC_MANAGER, { } },
    { "audio",      "Audio",            ATRACE_TAG_AUDIO, { } },
    { "video",      "Video",            ATRACE_TAG_VIDEO, { } },
    { "camera",     "Camera",           ATRACE_TAG_CAMERA, { } },
    { "hal",        "Hardware Modules", ATRACE_TAG_HAL, { } },
    { "app",        "Application",      ATRACE_TAG_APP, { } },
    { "res",        "Resource Loading", ATRACE_TAG_RESOURCES, { } },
    { "dalvik",     "Dalvik VM",        ATRACE_TAG_DALVIK, { } },
    { "rs",         "RenderScript",     ATRACE_TAG_RS, { } },
    { "bionic",     "Bionic C Library", ATRACE_TAG_BIONIC, { } },
    { "power",      "Power Management", ATRACE_TAG_POWER, { } },
    { "sched",      "CPU Scheduling",   0, {
        { REQ,      "/sys/kernel/debug/tracing/events/sched/sched_switch/enable" },
        { REQ,      "/sys/kernel/debug/tracing/events/sched/sched_wakeup/enable" },
    } },
    { "irq",        "IRQ Events",   0, {
        { REQ,      "/sys/kernel/debug/tracing/events/irq/enable" },
    } },
    { "freq",       "CPU Frequency",    0, {
        { REQ,      "/sys/kernel/debug/tracing/events/power/cpu_frequency/enable" },
        { OPT,      "/sys/kernel/debug/tracing/events/power/clock_set_rate/enable" },
    } },
    { "membus",     "Memory Bus Utilization", 0, {
        { REQ,      "/sys/kernel/debug/tracing/events/memory_bus/enable" },
    } },
    { "idle",       "CPU Idle",         0, {
        { REQ,      "/sys/kernel/debug/tracing/events/power/cpu_idle/enable" },
    } },
    { "disk",       "Disk I/O",         0, {
        { OPT,      "/sys/kernel/debug/tracing/events/f2fs/f2fs_sync_file_enter/enable" },
        { OPT,      "/sys/kernel/debug/tracing/events/f2fs/f2fs_sync_file_exit/enable" },
        { OPT,      "/sys/kernel/debug/tracing/events/f2fs/f2fs_write_begin/enable" },
        { OPT,      "/sys/kernel/debug/tracing/events/f2fs/f2fs_write_end/enable" },
        { OPT,      "/sys/kernel/debug/tracing/events/ext4/ext4_da_write_begin/enable" },
        { OPT,      "/sys/kernel/debug/tracing/events/ext4/ext4_da_write_end/enable" },
        { OPT,      "/sys/kernel/debug/tracing/events/ext4/ext4_sync_file_enter/enable" },
        { OPT,      "/sys/kernel/debug/tracing/events/ext4/ext4_sync_file_exit/enable" },
        { REQ,      "/sys/kernel/debug/tracing/events/block/block_rq_issue/enable" },
        { REQ,      "/sys/kernel/debug/tracing/events/block/block_rq_complete/enable" },
    } },
    { "mmc",        "eMMC commands",    0, {
        { REQ,      "/sys/kernel/debug/tracing/events/mmc/enable" },
    } },
    { "load",       "CPU Load",         0, {
        { REQ,      "/sys/kernel/debug/tracing/events/cpufreq_interactive/enable" },
    } },
    { "sync",       "Synchronization",  0, {
        { REQ,      "/sys/kernel/debug/tracing/events/sync/enable" },
    } },
    { "workq",      "Kernel Workqueues", 0, {
        { REQ,      "/sys/kernel/debug/tracing/events/workqueue/enable" },
    } },
    { "memreclaim", "Kernel Memory Reclaim", 0, {
        { REQ,      "/sys/kernel/debug/tracing/events/vmscan/mm_vmscan_direct_reclaim_begin/enable" },
        { REQ,      "/sys/kernel/debug/tracing/events/vmscan/mm_vmscan_direct_reclaim_end/enable" },
        { REQ,      "/sys/kernel/debug/tracing/events/vmscan/mm_vmscan_kswapd_wake/enable" },
        { REQ,      "/sys/kernel/debug/tracing/events/vmscan/mm_vmscan_kswapd_sleep/enable" },
    } },
    { "regulators",  "Voltage and Current Regulators", 0, {
        { REQ,      "/sys/kernel/debug/tracing/events/regulator/enable" },
    } },
};

对于Kernel的tracing就是直接设置sysfs中的开关,写1就打开tracing,写0就关闭。对于用户空间的tracing则使用了ATRACE_TAG,每一个category代表trace tags中的一位,使用0/1来表示使能/关闭。生成的trace tags被保存的属性"debug.atrace.tags.enableflags"中。在具体模块在使用tracing时,先定义本模块的ATRACE_TAG,然后在具体的函数入口增加ATRACE_CALL()来进行跟踪。例如在SurfaceFlinger的代码中可以看到,

#define ATRACE_TAG ATRACE_TAG_GRAPHICS
......
void SurfaceFlinger::eventControl(int disp, int event, int enabled) {
    ATRACE_CALL();
    getHwComposer().eventControl(disp, event, enabled);
}

void SurfaceFlinger::onMessageReceived(int32_t what) {
    ATRACE_CALL();
    ......
}

ATRACE_CALL()的实现很简单,就是在构造和析构时调用了atrace_begin()和atrace_end()。

#define ATRACE_NAME(name) android::ScopedTrace ___tracer(ATRACE_TAG, name)
// ATRACE_CALL is an ATRACE_NAME that uses the current function name.
#define ATRACE_CALL() ATRACE_NAME(__FUNCTION__)

namespace android {

class ScopedTrace {
public:
inline ScopedTrace(uint64_t tag, const char* name)
    : mTag(tag) {
    atrace_begin(mTag,name);
}

inline ~ScopedTrace() {
    atrace_end(mTag);
}

private:
    uint64_t mTag;
};

}; // namespace android

atrace_begin()实现就是写入一条开始信息到Ftrace的trace_marker中,其中包含进程id、trace名字等信息。trace_marker位于"/sys/kernel/debug/tracing/trace_marker"。atrace_end()写入一条结束信息。

#define ATRACE_BEGIN(name) atrace_begin(ATRACE_TAG, name)
static inline void atrace_begin(uint64_t tag, const char* name)
{
    if (CC_UNLIKELY(atrace_is_tag_enabled(tag))) {
        char buf[ATRACE_MESSAGE_LENGTH];
        size_t len;

        len = snprintf(buf, ATRACE_MESSAGE_LENGTH, "B|%d|%s", getpid(), name);
        write(atrace_marker_fd, buf, len);
    }   
}

#define ATRACE_END() atrace_end(ATRACE_TAG)
static inline void atrace_end(uint64_t tag)
{
    if (CC_UNLIKELY(atrace_is_tag_enabled(tag))) {
        char c = 'E';
        write(atrace_marker_fd, &c, 1); 
    }   
}

配置Ftrace

除了上述Ftrace的控制开关,还需要在tracing之前在用户空间对Ftrace进行配置。我们使用Atrace的源码来看一下配置过程,Atrace中涉及的配置如下。

// 配置tracing使用的时钟,"global":全局时钟,"local":per-CPU本地时钟。默认使用全局时钟。
static const char* k_traceClockPath =
    "/sys/kernel/debug/tracing/trace_clock";
// 配置tracing缓存的大小,默认为2048kb。在Systrace和Atrace命令中可以通过"-b"指定。
static const char* k_traceBufferSizePath =
    "/sys/kernel/debug/tracing/buffer_size_kb";
// 设置tracing缓存是否可以被覆盖,默认是false。在Atrace命令中可以通过"-c"设置。
static const char* k_tracingOverwriteEnablePath =
    "/sys/kernel/debug/tracing/options/overwrite";
// 设置使用的tracer,默认为"function_graph"。
static const char* k_currentTracerPath =
    "/sys/kernel/debug/tracing/current_tracer";
// tracing中是否打印tgdi,默认为true。
static const char* k_printTgidPath =
    "/sys/kernel/debug/tracing/options/print-tgid";
// 使用"function_graph"时是否显示绝对时间戳,默认为true。
static const char* k_funcgraphAbsTimePath =
    "/sys/kernel/debug/tracing/options/funcgraph-abstime";
// 使用"function_graph"时是否显示所在的CPU,默认为true。
static const char* k_funcgraphCpuPath =
    "/sys/kernel/debug/tracing/options/funcgraph-cpu";
// 使用"function_graph"时是否显示进程的command line,默认为true。
static const char* k_funcgraphProcPath =
    "/sys/kernel/debug/tracing/options/funcgraph-proc";
// 使用"function_graph"时是否显示为平面列表,默认为true。
static const char* k_funcgraphFlatPath =
    "/sys/kernel/debug/tracing/options/funcgraph-flat";
// 使用"function_graph"时是否显示函数执行时间,未设置。
static const char* k_funcgraphDurationPath =
    "/sys/kernel/debug/tracing/options/funcgraph-duration";
// 指定tracing的kernel函数,Systrace和Atrace命令可以通过"-k"来指定。
static const char* k_ftraceFilterPath =
    "/sys/kernel/debug/tracing/set_ftrace_filter";
// 开始或停止tracing,实际上是控制tracing缓存是否可以被写入。
static const char* k_tracingOnPath =
    "/sys/kernel/debug/tracing/tracing_on";
// tracing输出到这个节点,读取这个节点来dump输出。
static const char* k_tracePath =
    "/sys/kernel/debug/tracing/trace";
// 用户写这个节点来增加自己的tracing。
static const char* k_traceMarkerPath =
    "/sys/kernel/debug/tracing/trace_marker";

Atrace设置Ftrace在setUpTrace()完成,之后会调用startTrace()开始使能跟踪。

static bool setKernelTraceFuncs(const char* funcs)
{
    bool ok = true;

    if (funcs == NULL || funcs[0] == '\0') {
        // Disable kernel function tracing.
        if (fileIsWritable(k_currentTracerPath)) {
            ok &= writeStr(k_currentTracerPath, "nop"); //设置current_tracer
        }
        if (fileIsWritable(k_ftraceFilterPath)) {
            ok &= truncateFile(k_ftraceFilterPath);
        }
    } else {
        // Enable kernel function tracing.
        ok &= writeStr(k_currentTracerPath, "function_graph"); //设置current_tracer
        ok &= setKernelOptionEnable(k_funcgraphAbsTimePath, true); //设置funcgraph-abstime
        ok &= setKernelOptionEnable(k_funcgraphCpuPath, true); //设置funcgraph-cpu
        ok &= setKernelOptionEnable(k_funcgraphProcPath, true); //设置funcgraph-proc
        ok &= setKernelOptionEnable(k_funcgraphFlatPath, true); //设置funcgraph-flat

        // Set the requested filter functions.
        ...... 
        // Verify that the set functions are being traced.
        if (ok) {
            ok &= verifyKernelTraceFuncs(funcs); //设置set_ftrace_filter
        }
    }

    return ok;
}

static bool setUpTrace()
{
    bool ok = true;

    // Set up the tracing options.
    ok &= setTraceOverwriteEnable(g_traceOverwrite); //设置overwrite
    ok &= setTraceBufferSizeKB(g_traceBufferSizeKB); //设置buffer_size_kb
    ok &= setGlobalClockEnable(true); //设置trace_clock
    ok &= setPrintTgidEnableIfPresent(true); //设置print-tgid
    ok &= setKernelTraceFuncs(g_kernelTraceFuncs); 

    // Set up the tags property.
    ...... // 设置property

    // Enable all the sysfs enables that are in an enabled category.
    ...... // 通过sysfs使能kernel中的categories
        
    return ok;
}

Atrace的大致流程就是:配置Ftrace的参数,然后使能tracing,结束后抓取tracing数据。

Ftrace的使用

上面介绍了使用Systrace和Atrace进行系统跟踪的方法,它们本质上最终还是使用Ftrace来实现。所以,我们完全可以直接通过sysfs来配置Ftrace来进行系统跟踪,但这种方式仅仅可以跟踪kernel的调用。使用过程与Atrace的实现是类似,一个简单的Ftrace使用过程如下。

  1. 使能需要tracing的kernel事件,例如:

    $ echo 1 > /sys/kernel/debug/tracing/events/cpufreq_interactive/enable
  2. 选着tracer,

    $ echo function_graph > /sys/kernel/debug/tracing/current_tracer
  3. 配置缓存的大小,

    $ echo 2048 > /sys/kernel/debug/tracing/buffer_size_kb
  4. 使能tracing,

    $ echo 1 > /sys/kernel/debug/tracing/tracing_on
  5. 运行测试程序,然后停止tracing,

    $ echo 0 > /sys/kernel/debug/tracing/tracing_on
  6. 导出tracing数据,

    $ cat /sys/kernel/debug/tracing/trace > /data/tmp/trace_output

原始的tracing数据可读性较差,大致的样子如下。

TRACE:
# tracer: nop
#
# entries-in-buffer/entries-written: 1701/1701   #P:2
#
#                                      _-----=> irqs-off
#                                     / _----=> need-resched
#                                    | / _---=> hardirq/softirq
#                                    || / _--=> preempt-depth
#                                    ||| /     delay
#           TASK-PID    TGID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |        |      |   ||||       |         |
           <...>-7844  (-----) [000] ...1   886.340626: tracing_mark_write: B|3073|queueBuffer
           <...>-4061  (-----) [000] ...1   886.340908: tracing_mark_write: B|3049|queueBuffer
           <...>-4061  (-----) [000] ...1   886.340932: tracing_mark_write: B|3049|SurfaceView: 2

分析这个tracing数据时只能通过搜索去寻找感兴趣的方法,然后再进行数据对比。可以通过工具”Catapult“来提供可读性,Catapult可以将文本格式的数据转化为网页格式。通过GitHub来获取Catapult,然后运行trace2html生成网页文件。

$ catapult/tracing/bin/trace2html ~/path/to/trace_file
参考文档:

System Tracing in Android Developer

Android性能分析工具systrace使用

阅读 4.6k

老王系统屋
做为一个不称职的老年码农,一直疏忽整理笔记,开博记录一下,用来丰富老年生活,

做为一个不称职的老年码农,一直疏忽整理笔记,开博记录一下,用来丰富老年生活,

58 声望
13 粉丝
0 条评论
你知道吗?

做为一个不称职的老年码农,一直疏忽整理笔记,开博记录一下,用来丰富老年生活,

58 声望
13 粉丝
宣传栏